From 5414ec542d6158d82fb4310ce7631e1cf0090a1c Mon Sep 17 00:00:00 2001 From: Floris Westerman Date: Sun, 8 Nov 2020 20:28:06 +0100 Subject: [PATCH] Add pluggable identity system --- .../0.0.0/app/libraries/RainLoop/Actions.php | 115 +++------------ .../libraries/RainLoop/Actions/Accounts.php | 97 +++++-------- .../app/libraries/RainLoop/Model/Identity.php | 102 ++++++++----- .../RainLoop/Providers/Identities.php | 136 ++++++++++++++++++ .../Providers/Identities/FileIdentities.php | 69 +++++++++ .../Providers/Identities/IIdentities.php | 34 +++++ .../Providers/Identities/TestIdentities.php | 44 ++++++ .../Providers/Suggestions/TestSuggestions.php | 6 +- 8 files changed, 418 insertions(+), 185 deletions(-) create mode 100755 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities.php create mode 100755 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/FileIdentities.php create mode 100755 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/IIdentities.php create mode 100755 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/TestIdentities.php diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 8a46f43ba..a815a39f4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -4,6 +4,9 @@ namespace RainLoop; use RainLoop\Enumerations\UploadError; use RainLoop\Enumerations\UploadClientError; +use RainLoop\Model\Identity; +use RainLoop\Providers\Identities; +use RainLoop\Providers\Storage\Enumerations\StorageType; class Actions { @@ -55,6 +58,11 @@ class Actions */ private $aCachers; + /** + * @var Providers\Identities + */ + private $oIdentitiesProvider; + /** * @var \RainLoop\Providers\Storage */ @@ -282,12 +290,6 @@ class Actions } break; case 'suggestions': - - if (null === $mResult) - { - $mResult = array(); - } - break; case 'two-factor-auth': // Providers\TwoFactorAuth\TwoFactorAuthInterface @@ -296,6 +298,12 @@ class Actions } } + // Always give the file provider as last for identities, it is the override + if($sName === 'identities') { + if($mResult === null) $mResult = []; + $mResult[] = new Providers\Identities\FileIdentities($this->StorageProvider(true)); + } + foreach (\is_array($mResult) ? $mResult : array($mResult) as $oItem) { if ($oItem && \method_exists($oItem, 'SetLogger')) @@ -633,10 +641,17 @@ class Actions return $this->oStorageProvider; } - - return null; } + public function IdentitiesProvider() : Identities + { + if(null === $this->oIdentitiesProvider) { + $this->oIdentitiesProvider = new Providers\Identities($this->fabrica('identities')); + } + + return $this->oIdentitiesProvider; + } + public function SettingsProvider(bool $bLocal = false) : Providers\Settings { if ($bLocal) @@ -659,8 +674,6 @@ class Actions return $this->oSettingsProvider; } - - return null; } public function FilesProvider() : Providers\Files @@ -1867,88 +1880,6 @@ NewThemeLink IncludeCss TemplatesLink LangLink IncludeBackground PluginsLink Aut return $aAccounts; } - public function GetIdentities(Model\Account $oAccount) : array - { - $bAllowIdentities = $this->GetCapa(false, false, - Enumerations\Capa::IDENTITIES, $oAccount); - - $aIdentities = array(); - if ($oAccount) - { - $aSubIdentities = array(); - - $sData = $this->StorageProvider(true)->Get($oAccount, - Providers\Storage\Enumerations\StorageType::CONFIG, - 'identities' - ); - - if ('' !== $sData && '[' === \substr($sData, 0, 1)) - { - $aSubIdentities = \json_decode($sData, true); - } - - $bHasAccountIdentity = false; - - if (\is_array($aSubIdentities) && 0 < \count($aSubIdentities)) - { - foreach ($aSubIdentities as $aItem) - { - $oItem = new Model\Identity(); - $oItem->FromJSON($aItem); - - if ($oItem && $oItem->Validate()) - { - if ($oItem->IsAccountIdentities()) - { - $oItem->SetEmail($oAccount->Email()); - $bHasAccountIdentity = true; - - \array_push($aIdentities, $oItem); - } - else if ($bAllowIdentities) - { - \array_push($aIdentities, $oItem); - } - } - } - } - - if (!$bHasAccountIdentity) - { - \array_unshift($aIdentities, - new Model\Identity('', $oAccount->Email())); - } - - if (1 < \count($aIdentities) && $bAllowIdentities) - { - $sOrder = $this->StorageProvider()->Get($oAccount, - Providers\Storage\Enumerations\StorageType::CONFIG, - 'accounts_identities_order' - ); - - $aOrder = empty($sOrder) ? array() : \json_decode($sOrder, true); - if (isset($aOrder['Identities']) && \is_array($aOrder['Identities']) && - 1 < \count($aOrder['Identities'])) - { - $aList = $aOrder['Identities']; - foreach ($aList as $iIndex => $sItem) - { - if ('' === $sItem) - { - $aList[$iIndex] = '---'; - } - } - - \usort($aIdentities, function ($a, $b) use ($aList) { - return \array_search($a->Id(true), $aList) < \array_search($b->Id(true), $aList) ? -1 : 1; - }); - } - } - } - - return $aIdentities; - } - public function GetIdentityByID(Model\Account $oAccount, string $sID, bool $bFirstOnEmpty = false) : ?Model\Identity { $aIdentities = $this->GetIdentities($oAccount); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php index a66f8c1fc..c02cbe9f4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php @@ -4,7 +4,11 @@ namespace RainLoop\Actions; 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 { @@ -25,7 +29,7 @@ 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'); @@ -65,7 +69,7 @@ trait Accounts } $sParentEmail = $oAccount->ParentEmailHelper(); - $sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', '')); + $sEmailToDelete = trim($this->GetActionParam('EmailToDelete', '')); $sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true); $aAccounts = $this->GetAccounts($oAccount); @@ -104,32 +108,8 @@ trait Accounts 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); } /** @@ -144,24 +124,14 @@ trait Accounts return $this->FalseResponse(__FUNCTION__); } - $sId = \trim($this->GetActionParam('IdToDelete', '')); + $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); } /** @@ -214,26 +184,35 @@ trait Accounts )); } - private function SetIdentities(\RainLoop\Model\Account $oAccount, array $aIdentities = array()) : bool - { - $bAllowIdentities = $this->GetCapa(false, false, Capa::IDENTITIES, $oAccount); + /** + * @param Account $account + * @return Identity[] + */ + public function GetIdentities(Account $account) : array + { + 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); - return $this->StorageProvider(true)->Put($oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, - 'identities', - \json_encode($aResult) - ); - } + // 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 $identities; + } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php index 54023f46b..c55eb79ca 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php @@ -2,42 +2,45 @@ namespace RainLoop\Model; -class Identity implements \JsonSerializable +use JsonSerializable; +use MailSo\Base\Utils; + +class Identity implements JsonSerializable { - /** - * @var string - */ - private $sId; + /** + * @var string + */ + private $sId; - /** - * @var string - */ - private $sEmail; + /** + * @var string + */ + private $sEmail; - /** - * @var string - */ - private $sName; + /** + * @var string + */ + private $sName; - /** - * @var string - */ - private $sReplyTo; + /** + * @var string + */ + private $sReplyTo; - /** - * @var string - */ - private $sBcc; + /** + * @var string + */ + private $sBcc; - /** - * @var string - */ - private $sSignature; + /** + * @var string + */ + private $sSignature; - /** - * @var bool - */ - private $bSignatureInsertBefore; + /** + * @var bool + */ + private $bSignatureInsertBefore; function __construct(string $sId = '', string $sEmail = '') { @@ -92,12 +95,48 @@ class Identity implements \JsonSerializable return $this->bSignatureInsertBefore; } + public function SetId(string $sId): Identity + { + $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'] : ''; @@ -127,9 +166,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(), diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities.php new file mode 100755 index 000000000..910366f50 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities.php @@ -0,0 +1,136 @@ +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)); + + file_put_contents('php://stderr', print_r($this->identitiesPerDriverPerAccount, TRUE)); + + // Find the primary identity + $primaryIdentity = array_filter($identities, function($identity) { + return $identity->IsAccountIdentities(); + })[0]; + + // If no primary identity is found, generate default one from account info + if($primaryIdentity === null) + $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 = array_filter($this->drivers, function($driver) { + return $driver->SupportsStore(); + })[0]; + + $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); + } +} \ No newline at end of file diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/FileIdentities.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/FileIdentities.php new file mode 100755 index 000000000..1a1d5cbee --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/FileIdentities.php @@ -0,0 +1,69 @@ +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"; + } +} \ No newline at end of file diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/IIdentities.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/IIdentities.php new file mode 100755 index 000000000..524312d6c --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/IIdentities.php @@ -0,0 +1,34 @@ +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"; } +} \ No newline at end of file diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/TestSuggestions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/TestSuggestions.php index c92f4f224..51490bc54 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/TestSuggestions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/TestSuggestions.php @@ -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(), ''),