diff options
author | Daniel Kesselberg <mail@danielkesselberg.de> | 2021-06-16 15:39:03 +0300 |
---|---|---|
committer | Daniel Kesselberg <mail@danielkesselberg.de> | 2021-07-06 23:04:31 +0300 |
commit | 8fb14b5ffa3450b2c2201ff9bb42c589e4a4afb7 (patch) | |
tree | a5b84701a79eca0784c96a87b37bb20127c4ba86 /lib | |
parent | 70ef1726ad78b25db571e4b8096eaaf2fa2d84c2 (diff) |
Add provisioning for aliases
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Controller/AliasesController.php | 9 | ||||
-rw-r--r-- | lib/Controller/SettingsController.php | 9 | ||||
-rw-r--r-- | lib/Db/Alias.php | 12 | ||||
-rw-r--r-- | lib/Db/AliasMapper.php | 45 | ||||
-rw-r--r-- | lib/Db/Provisioning.php | 14 | ||||
-rw-r--r-- | lib/Db/ProvisioningMapper.php | 23 | ||||
-rw-r--r-- | lib/Migration/Version1101Date20210616141806.php | 34 | ||||
-rw-r--r-- | lib/Service/AliasesService.php | 44 | ||||
-rw-r--r-- | lib/Service/Provisioning/Manager.php | 167 | ||||
-rw-r--r-- | lib/Settings/AdminSettings.php | 9 |
10 files changed, 297 insertions, 69 deletions
diff --git a/lib/Controller/AliasesController.php b/lib/Controller/AliasesController.php index 8c1b49ddc..add3efb5f 100644 --- a/lib/Controller/AliasesController.php +++ b/lib/Controller/AliasesController.php @@ -72,8 +72,8 @@ class AliasesController extends Controller { * @NoAdminRequired * @TrapError */ - public function update() { - throw new NotImplemented(); + public function update(int $id, string $alias, string $aliasName): JSONResponse { + return new JSONResponse($this->aliasService->update($this->currentUserId, $id, $alias, $aliasName)); } /** @@ -84,7 +84,7 @@ class AliasesController extends Controller { * @return JSONResponse */ public function destroy(int $id): JSONResponse { - return new JSONResponse($this->aliasService->delete($id, $this->currentUserId)); + return new JSONResponse($this->aliasService->delete($this->currentUserId, $id)); } /** @@ -116,7 +116,6 @@ class AliasesController extends Controller { * @throws DoesNotExistException */ public function updateSignature(int $id, string $signature = null): JSONResponse { - $this->aliasService->updateSignature($this->currentUserId, $id, $signature); - return new JSONResponse(); + return new JSONResponse($this->aliasService->updateSignature($this->currentUserId, $id, $signature)); } } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 2081ea5bb..dd1a19dc4 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -60,6 +60,8 @@ class SettingsController extends Controller { $this->provisioningManager->newProvisioning($data); } catch (ValidationException $e) { return HttpJsonResponse::fail([$e->getFields()]); + } catch (\Exception $e) { + return HttpJsonResponse::fail([$e->getMessage()]); } return new JSONResponse([]); @@ -67,12 +69,11 @@ class SettingsController extends Controller { public function updateProvisioning(int $id, array $data): JSONResponse { try { - $this->provisioningManager->updateProvisioning(array_merge( - $data, - ['id' => $id] - )); + $this->provisioningManager->updateProvisioning(array_merge($data, ['id' => $id])); } catch (ValidationException $e) { return HttpJsonResponse::fail([$e->getFields()]); + } catch (\Exception $e) { + return HttpJsonResponse::fail([$e->getMessage()]); } return new JSONResponse([]); diff --git a/lib/Db/Alias.php b/lib/Db/Alias.php index e1727a83c..812682678 100644 --- a/lib/Db/Alias.php +++ b/lib/Db/Alias.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace OCA\Mail\Db; use JsonSerializable; - use OCP\AppFramework\Db\Entity; /** @@ -36,6 +35,8 @@ use OCP\AppFramework\Db\Entity; * @method string getAlias() * @method void setSignature(string|null $signature) * @method string|null getSignature() + * @method void setProvisioningId(int $provisioningId) + * @method int|null getProvisioningId() */ class Alias extends Entity implements JsonSerializable { @@ -51,10 +52,18 @@ class Alias extends Entity implements JsonSerializable { /** @var string|null */ protected $signature; + /** @var int|null */ + protected $provisioningId; + public function __construct() { $this->addType('accountId', 'int'); $this->addType('name', 'string'); $this->addType('alias', 'string'); + $this->addType('provisioningId', 'int'); + } + + public function isProvisioned(): bool { + return $this->getProvisioningId() !== null; } public function jsonSerialize(): array { @@ -63,6 +72,7 @@ class Alias extends Entity implements JsonSerializable { 'name' => $this->getName(), 'alias' => $this->getAlias(), 'signature' => $this->getSignature(), + 'provisioned' => $this->isProvisioned(), ]; } } diff --git a/lib/Db/AliasMapper.php b/lib/Db/AliasMapper.php index bf2e17b5d..ad95fc5b6 100644 --- a/lib/Db/AliasMapper.php +++ b/lib/Db/AliasMapper.php @@ -41,7 +41,7 @@ class AliasMapper extends QBMapper { */ public function find(int $aliasId, string $currentUserId): Alias { $qb = $this->db->getQueryBuilder(); - $qb->select('aliases.*') + $qb->select('aliases.*', 'accounts.provisioning_id') ->from($this->getTableName(), 'aliases') ->join('aliases', 'mail_accounts', 'accounts', $qb->expr()->eq('aliases.account_id', 'accounts.id')) ->where( @@ -55,6 +55,24 @@ class AliasMapper extends QBMapper { } /** + * @throws DoesNotExistException + */ + public function findByAlias(string $alias, string $currentUserId): Alias { + $qb = $this->db->getQueryBuilder(); + $qb->select('aliases.*', 'accounts.provisioning_id') + ->from($this->getTableName(), 'aliases') + ->join('aliases', 'mail_accounts', 'accounts', $qb->expr()->eq('aliases.account_id', 'accounts.id')) + ->where( + $qb->expr()->andX( + $qb->expr()->eq('accounts.user_id', $qb->createNamedParameter($currentUserId)), + $qb->expr()->eq('aliases.alias', $qb->createNamedParameter($alias)) + ) + ); + + return $this->findEntity($qb); + } + + /** * @param int $accountId * @param string $currentUserId * @@ -62,7 +80,7 @@ class AliasMapper extends QBMapper { */ public function findAll(int $accountId, string $currentUserId): array { $qb = $this->db->getQueryBuilder(); - $qb->select('aliases.*') + $qb->select('aliases.*', 'accounts.provisioning_id') ->from($this->getTableName(), 'aliases') ->join('aliases', 'mail_accounts', 'accounts', $qb->expr()->eq('aliases.account_id', 'accounts.id')) ->where( @@ -89,6 +107,29 @@ class AliasMapper extends QBMapper { $query->execute(); } + /** + * Delete all provisioned aliases for the given uid + * + * Exception for Nextcloud 20: \Doctrine\DBAL\DBALException + * Exception for Nextcloud 21 and newer: \OCP\DB\Exception + * + * @TODO: Change throws to \OCP\DB\Exception once Mail does not support Nextcloud 20. + * + * @throws \Exception + */ + public function deleteProvisionedAliasesByUid(string $uid): void { + $qb = $this->db->getQueryBuilder(); + + $qb->delete($this->getTableName(), 'aliases') + ->join('aliases', 'mail_accounts', 'accounts', 'accounts.id = aliases.account_id') + ->where( + $qb->expr()->eq('accounts.user_id', $qb->createNamedParameter($uid)), + $qb->expr()->isNotNull('provisioning_id') + ); + + $qb->execute(); + } + public function deleteOrphans(): void { $qb1 = $this->db->getQueryBuilder(); $idsQuery = $qb1->select('a.id') diff --git a/lib/Db/Provisioning.php b/lib/Db/Provisioning.php index 97d3c7a81..b29afa91c 100644 --- a/lib/Db/Provisioning.php +++ b/lib/Db/Provisioning.php @@ -60,6 +60,12 @@ use OCP\IUser; * @method void setSieveSslMode(?string $sieveSslMode) * @method string|null getSieveUser() * @method void setSieveUser(?string $sieveUser) + * @method array getAliases() + * @method void setAliases(array $aliases) + * @method bool getLdapAliasesProvisioning() + * @method void setLdapAliasesProvisioning(bool $ldapAliasesProvisioning) + * @method string|null getLdapAliasesAttribute() + * @method void setLdapAliasesAttribute(?string $ldapAliasesAttribute) */ class Provisioning extends Entity implements JsonSerializable { public const WILDCARD = '*'; @@ -79,13 +85,18 @@ class Provisioning extends Entity implements JsonSerializable { protected $sieveHost; protected $sievePort; protected $sieveSslMode; + protected $aliases = []; + protected $ldapAliasesProvisioning; + protected $ldapAliasesAttribute; public function __construct() { $this->addType('imapPort', 'integer'); $this->addType('smtpPort', 'integer'); $this->addType('sieveEnabled', 'boolean'); $this->addType('sievePort', 'integer'); + $this->addType('ldapAliasesProvisioning', 'boolean'); } + /** * @return array */ @@ -107,6 +118,9 @@ class Provisioning extends Entity implements JsonSerializable { 'sieveHost' => $this->getSieveHost(), 'sievePort' => $this->getSievePort(), 'sieveSslMode' => $this->getSieveSslMode(), + 'aliases' => $this->getAliases(), + 'ldapAliasesProvisioning' => $this->getLdapAliasesProvisioning(), + 'ldapAliasesAttribute' => $this->getLdapAliasesAttribute(), ]; } diff --git a/lib/Db/ProvisioningMapper.php b/lib/Db/ProvisioningMapper.php index 09eec6281..0daca261f 100644 --- a/lib/Db/ProvisioningMapper.php +++ b/lib/Db/ProvisioningMapper.php @@ -51,11 +51,11 @@ class ProvisioningMapper extends QBMapper { * * @return Provisioning[] */ - public function getAll() : array { + public function getAll(): array { $qb = $this->db->getQueryBuilder(); $qb = $qb->select('*') - ->from($this->getTableName()) - ->orderBy('provisioning_domain', 'desc'); + ->from($this->getTableName()) + ->orderBy('provisioning_domain', 'desc'); try { return $this->findEntities($qb); } catch (DoesNotExistException $e) { @@ -69,7 +69,7 @@ class ProvisioningMapper extends QBMapper { * @return Provisioning * @throws ValidationException */ - public function validate(array $data) : Provisioning { + public function validate(array $data): Provisioning { $exception = new ValidationException(); if (!isset($data['provisioningDomain']) || $data['provisioningDomain'] === '') { @@ -103,11 +103,17 @@ class ProvisioningMapper extends QBMapper { $exception->setField('smtpSslMode', false); } + $ldapAliasesProvisioning = (bool)($data['ldapAliasesProvisioning'] ?? false); + $ldapAliasesAttribute = $data['ldapAliasesAttribute'] ?? ''; + + if ($ldapAliasesProvisioning && empty($ldapAliasesAttribute)) { + $exception->setField('ldapAliasesAttribute', false); + } + if (!empty($exception->getFields())) { throw $exception; } - $provisioning = new Provisioning(); $provisioning->setId($data['id'] ?? null); $provisioning->setProvisioningDomain($data['provisioningDomain']); @@ -127,14 +133,17 @@ class ProvisioningMapper extends QBMapper { $provisioning->setSievePort($data['sievePort'] ?? null); $provisioning->setSieveSslMode($data['sieveSslMode'] ?? ''); + $provisioning->setLdapAliasesProvisioning($ldapAliasesProvisioning); + $provisioning->setLdapAliasesAttribute($ldapAliasesAttribute); + return $provisioning; } public function get(int $id): ?Provisioning { $qb = $this->db->getQueryBuilder(); $qb = $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id), IQueryBuilder::PARAM_INT)); + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id), IQueryBuilder::PARAM_INT)); try { return $this->findEntity($qb); } catch (DoesNotExistException $e) { diff --git a/lib/Migration/Version1101Date20210616141806.php b/lib/Migration/Version1101Date20210616141806.php new file mode 100644 index 000000000..0687b9091 --- /dev/null +++ b/lib/Migration/Version1101Date20210616141806.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use Doctrine\DBAL\Schema\SchemaException; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1101Date20210616141806 extends SimpleMigrationStep { + /** + * @throws SchemaException + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $provisioningTable = $schema->getTable('mail_provisionings'); + $provisioningTable->addColumn('ldap_aliases_provisioning', 'boolean', [ + 'notnull' => false, + 'default' => false + ]); + $provisioningTable->addColumn('ldap_aliases_attribute', 'string', [ + 'notnull' => false, + 'length' => 255, + 'default' => '', + ]); + + return $schema; + } +} diff --git a/lib/Service/AliasesService.php b/lib/Service/AliasesService.php index db21e1388..7d308b97c 100644 --- a/lib/Service/AliasesService.php +++ b/lib/Service/AliasesService.php @@ -26,6 +26,7 @@ namespace OCA\Mail\Service; use OCA\Mail\Db\Alias; use OCA\Mail\Db\AliasMapper; use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ClientException; use OCP\AppFramework\Db\DoesNotExistException; class AliasesService { @@ -81,15 +82,17 @@ class AliasesService { } /** - * @param int $aliasId - * @param String $currentUserId - * @return Alias + * @throws ClientException * @throws DoesNotExistException */ - public function delete(int $aliasId, string $currentUserId): Alias { - $alias = $this->aliasMapper->find($aliasId, $currentUserId); - $this->aliasMapper->delete($alias); - return $alias; + public function delete(string $userId, int $aliasId): Alias { + $entity = $this->aliasMapper->find($aliasId, $userId); + + if ($entity->isProvisioned()) { + throw new ClientException('Deleting a provisioned alias is not allowed.'); + } + + return $this->aliasMapper->delete($entity); } /** @@ -105,16 +108,29 @@ class AliasesService { } /** + * Update alias and name + * + * @throws DoesNotExistException + */ + public function update(string $userId, int $aliasId, string $alias, string $aliasName): Alias { + $entity = $this->aliasMapper->find($aliasId, $userId); + + if (!$entity->isProvisioned()) { + $entity->setAlias($alias); + } + $entity->setName($aliasName); + + return $this->aliasMapper->update($entity); + } + + /** * Update signature for alias * - * @param string $userId - * @param int $aliasId - * @param string|null $signature * @throws DoesNotExistException */ - public function updateSignature(string $userId, int $aliasId, string $signature = null): void { - $alias = $this->find($aliasId, $userId); - $alias->setSignature($signature); - $this->aliasMapper->update($alias); + public function updateSignature(string $userId, int $aliasId, string $signature = null): Alias { + $entity = $this->find($aliasId, $userId); + $entity->setSignature($signature); + return $this->aliasMapper->update($entity); } } diff --git a/lib/Service/Provisioning/Manager.php b/lib/Service/Provisioning/Manager.php index 8cc9c2e06..6f06c330e 100644 --- a/lib/Service/Provisioning/Manager.php +++ b/lib/Service/Provisioning/Manager.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace OCA\Mail\Service\Provisioning; use Horde_Mail_Rfc822_Address; +use OCA\Mail\Db\Alias; +use OCA\Mail\Db\AliasMapper; use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; use OCA\Mail\Db\Provisioning; @@ -33,6 +35,8 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\IUser; use OCP\IUserManager; +use OCP\LDAP\ILDAPProvider; +use OCP\LDAP\ILDAPProviderFactory; use OCP\Security\ICrypto; use Psr\Log\LoggerInterface; @@ -50,6 +54,12 @@ class Manager { /** @var ICrypto */ private $crypto; + /** @var ILDAPProviderFactory */ + private $ldapProviderFactory; + + /** @var AliasMapper */ + private $aliasMapper; + /** @var LoggerInterface */ private $logger; @@ -57,11 +67,15 @@ class Manager { ProvisioningMapper $provisioningMapper, MailAccountMapper $mailAccountMapper, ICrypto $crypto, + ILDAPProviderFactory $ldapProviderFactory, + AliasMapper $aliasMapper, LoggerInterface $logger) { $this->userManager = $userManager; $this->provisioningMapper = $provisioningMapper; $this->mailAccountMapper = $mailAccountMapper; $this->crypto = $crypto; + $this->ldapProviderFactory = $ldapProviderFactory; + $this->aliasMapper = $aliasMapper; $this->logger = $logger; } @@ -85,6 +99,75 @@ class Manager { } /** + * Delete orphaned aliases for the given account. + * + * A alias is orphaned if not listed in newAliases anymore + * (=> the provisioning configuration does contain it anymore) + * + * Exception for Nextcloud 20: \Doctrine\DBAL\DBALException + * Exception for Nextcloud 21 and newer: \OCP\DB\Exception + * + * @TODO: Change throws to \OCP\DB\Exception once Mail requires Nextcloud 21 or above + * + * @throws \Exception + */ + private function deleteOrphanedAliases(string $userId, int $accountId, array $newAliases): void { + $existingAliases = $this->aliasMapper->findAll($accountId, $userId); + foreach ($existingAliases as $existingAlias) { + if (!in_array($existingAlias->getAlias(), $newAliases, true)) { + $this->aliasMapper->delete($existingAlias); + } + } + } + + /** + * Create new aliases for the given account. + * + * Exception for Nextcloud 20: \Doctrine\DBAL\DBALException + * Exception for Nextcloud 21 and newer: \OCP\DB\Exception + * + * @TODO: Change throws to \OCP\DB\Exception once Mail requires Nextcloud 21 or above + * + * @throws \Exception + */ + private function createNewAliases(string $userId, int $accountId, array $newAliases, string $displayName): void { + foreach ($newAliases as $newAlias) { + try { + $this->aliasMapper->findByAlias($newAlias, $userId); + } catch (DoesNotExistException $e) { + $alias = new Alias(); + $alias->setAccountId($accountId); + $alias->setName($displayName); + $alias->setAlias($newAlias); + $this->aliasMapper->insert($alias); + } + } + } + + /** + * @throws \Exception if user id was not found in LDAP + * + * @TODO: Remove psalm-suppress once Mail requires Nextcloud 22 or above + */ + public function ldapAliasesIntegration(Provisioning $provisioning, IUser $user): Provisioning { + if ($user->getBackendClassName() !== 'LDAP' || $provisioning->getLdapAliasesProvisioning() === false || empty($provisioning->getLdapAliasesAttribute())) { + return $provisioning; + } + + /** @psalm-suppress UndefinedInterfaceMethod */ + if ($this->ldapProviderFactory->isAvailable() === false) { + $this->logger->debug('Request to provision mail aliases but ldap not available'); + return $provisioning; + } + + $ldapProvider = $this->ldapProviderFactory->getLDAPProvider(); + /** @psalm-suppress UndefinedInterfaceMethod */ + $provisioning->setAliases($ldapProvider->getMultiValueUserAttribute($user->getUID(), $provisioning->getLdapAliasesAttribute())); + + return $provisioning; + } + + /** * @param Provisioning[] $provisionings */ public function provisionSingleUser(array $provisionings, IUser $user): bool { @@ -96,57 +179,69 @@ class Manager { try { // TODO: match by UID only, catch multiple objects returned below and delete all those accounts - $existing = $this->mailAccountMapper->findProvisionedAccount($user); + $mailAccount = $this->mailAccountMapper->findProvisionedAccount($user); - $this->mailAccountMapper->update( - $this->updateAccount($user, $existing, $provisioning) + $mailAccount = $this->mailAccountMapper->update( + $this->updateAccount($user, $mailAccount, $provisioning) ); - return true; - } catch (DoesNotExistException $e) { + } catch (DoesNotExistException | MultipleObjectsReturnedException $e) { + if ($e instanceof MultipleObjectsReturnedException) { + // This is unlikely to happen but not impossible. + // Let's wipe any existing accounts and start fresh + $this->aliasMapper->deleteProvisionedAliasesByUid($user->getUID()); + $this->mailAccountMapper->deleteProvisionedAccountsByUid($user->getUID()); + } + // Fine, then we create a new one - $new = new MailAccount(); - $new->setUserId($user->getUID()); + $mailAccount = new MailAccount(); + $mailAccount->setUserId($user->getUID()); - $this->mailAccountMapper->insert( - $this->updateAccount($user, $new, $provisioning) + $mailAccount = $this->mailAccountMapper->insert( + $this->updateAccount($user, $mailAccount, $provisioning) ); - return true; - } catch (MultipleObjectsReturnedException $e) { - // This is unlikely to happen but not impossible. - // Let's wipe any existing accounts and start fresh - $this->mailAccountMapper->deleteProvisionedAccountsByUid($user->getUID()); + } - $new = new MailAccount(); - $new->setUserId($user->getUID()); + // @TODO: Remove method_exists once Mail requires Nextcloud 22 or above + if (method_exists(ILDAPProvider::class, 'getMultiValueUserAttribute')) { + try { + $provisioning = $this->ldapAliasesIntegration($provisioning, $user); + } catch (\Throwable $e) { + $this->logger->warning('Request to provision mail aliases failed', ['exception' => $e]); + // return here to avoid provisioning of aliases. + return true; + } - $this->mailAccountMapper->insert( - $this->updateAccount($user, $new, $provisioning) - ); - return true; + try { + $this->deleteOrphanedAliases($user->getUID(), $mailAccount->getId(), $provisioning->getAliases()); + } catch (\Throwable $e) { + $this->logger->warning('Deleting orphaned aliases failed', ['exception' => $e]); + } + + try { + $this->createNewAliases($user->getUID(), $mailAccount->getId(), $provisioning->getAliases(), $user->getDisplayName()); + } catch (\Throwable $e) { + $this->logger->warning('Creating new aliases failed', ['exception' => $e]); + } } - return false; + + return true; } + /** + * @throws ValidationException + * @throws \Exception + */ public function newProvisioning(array $data): void { - try { - $provisioning = $this->provisioningMapper->validate( - $data - ); - } catch (ValidationException $e) { - throw $e; - } + $provisioning = $this->provisioningMapper->validate($data); $this->provisioningMapper->insert($provisioning); } + /** + * @throws ValidationException + * @throws \Exception + */ public function updateProvisioning(array $data): void { - try { - $provisioning = $this->provisioningMapper->validate( - $data - ); - } catch (ValidationException $e) { - throw $e; - } - + $provisioning = $this->provisioningMapper->validate($data); $this->provisioningMapper->update($provisioning); } diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 2a8d0fd4e..428201244 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -29,6 +29,7 @@ use OCA\Mail\AppInfo\Application; use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\IInitialStateService; +use OCP\LDAP\ILDAPProvider; use OCP\Settings\ISettings; class AdminSettings implements ISettings { @@ -52,6 +53,14 @@ class AdminSettings implements ISettings { $this->provisioningManager->getConfigs() ); + $this->initialStateService->provideLazyInitialState( + Application::APP_ID, + 'ldap_aliases_integration', + function () { + return method_exists(ILDAPProvider::class, 'getMultiValueUserAttribute'); + } + ); + return new TemplateResponse(Application::APP_ID, 'settings-admin'); } |