From a178acfa1c786e8ef1f9dbcddc43dabd985be5a3 Mon Sep 17 00:00:00 2001 From: Micke Nordin Date: Thu, 6 Mar 2025 14:13:55 +0100 Subject: [PATCH] feat(OCM-invites): Add entity and mapper This patch introduces an entity and a mapper for Invites Signed-off-by: Micke Nordin --- .../composer/composer/autoload_classmap.php | 5 +- .../composer/composer/autoload_static.php | 5 +- .../Controller/RequestHandlerController.php | 125 ++++++++---------- .../lib/Db/FederatedInvite.php | 93 +++++++++++++ .../lib/Db/FederatedInviteMapper.php | 40 ++++++ .../Events/FederatedInviteAcceptedEvent.php | 24 ++++ .../lib/Events/OCMInvitationAcceptedEvent.php | 26 ---- .../Migration/Version1016Date202502262004.php | 16 +-- .../lib/OCMInvitation.php | 18 --- lib/composer/composer/autoload_psr4.php | 1 + lib/composer/composer/autoload_static.php | 8 ++ lib/public/OCM/IOCMProvider.php | 6 +- 12 files changed, 240 insertions(+), 127 deletions(-) create mode 100644 apps/cloud_federation_api/lib/Db/FederatedInvite.php create mode 100644 apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php create mode 100644 apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php delete mode 100644 apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php delete mode 100644 apps/cloud_federation_api/lib/OCMInvitation.php diff --git a/apps/cloud_federation_api/composer/composer/autoload_classmap.php b/apps/cloud_federation_api/composer/composer/autoload_classmap.php index aa0c7c5ad79..3cadc540c88 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_classmap.php +++ b/apps/cloud_federation_api/composer/composer/autoload_classmap.php @@ -11,8 +11,9 @@ return array( 'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php', + 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => $baseDir . '/../lib/Events/FederatedInviteAcceptedEvent.php', 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.php', 'OCA\\CloudFederationAPI\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', - 'OCA\\CloudFederationApi\\Events\\OCMInvitationAcceptedEvent' => $baseDir . '/../lib/Events/OCMInvitationAcceptedEvent.php', - 'OCA\\CloudFederationApi\\OCMInvitation' => $baseDir . '/../lib/OCMInvitation.php', ); diff --git a/apps/cloud_federation_api/composer/composer/autoload_static.php b/apps/cloud_federation_api/composer/composer/autoload_static.php index 133580e463e..849b755cd2f 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_static.php +++ b/apps/cloud_federation_api/composer/composer/autoload_static.php @@ -26,10 +26,11 @@ class ComposerStaticInitCloudFederationAPI 'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php', + 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/FederatedInviteAcceptedEvent.php', 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php', 'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', - 'OCA\\CloudFederationApi\\Events\\OCMInvitationAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/OCMInvitationAcceptedEvent.php', - 'OCA\\CloudFederationApi\\OCMInvitation' => __DIR__ . '/..' . '/../lib/OCMInvitation.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index 636f78d0a9d..8a4a2e04187 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -18,8 +18,8 @@ use NCU\Security\Signature\IIncomingSignedRequest; use NCU\Security\Signature\ISignatureManager; use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; -use OCA\CloudFederationAPI\Events\OCMInvitationAcceptedEvent; -use OCA\CloudFederationAPI\OCMInvitation; +use OCA\CloudFederationAPI\Db\FederatedInviteMapper; +use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent; use OCA\CloudFederationAPI\ResponseDefinitions; use OCA\FederatedFileSharing\AddressHandler; use OCA\Federation\TrustedServers; @@ -41,7 +41,6 @@ use OCP\Federation\ICloudFederationFactory; use OCP\Federation\ICloudFederationProviderManager; use OCP\Federation\ICloudIdManager; use OCP\IAppConfig; -use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IRequest; use OCP\IURLGenerator; @@ -60,8 +59,7 @@ use Psr\Log\LoggerInterface; * @psalm-import-type CloudFederationAPIError from ResponseDefinitions */ #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] -class RequestHandlerController extends Controller -{ +class RequestHandlerController extends Controller { public function __construct( string $appName, IRequest $request, @@ -72,14 +70,14 @@ class RequestHandlerController extends Controller private ICloudFederationProviderManager $cloudFederationProviderManager, private Config $config, private IEventDispatcher $dispatcher, - private IDBConnection $db, + private FederatedInviteMapper $federatedInviteMapper, private readonly AddressHandler $addressHandler, private readonly IAppConfig $appConfig, private ICloudFederationFactory $factory, private ICloudIdManager $cloudIdManager, private readonly ISignatureManager $signatureManager, private readonly OCMSignatoryManager $signatoryManager, - private TrustedServers $trustedServers + private TrustedServers $trustedServers, ) { parent::__construct($appName, $request); } @@ -108,8 +106,7 @@ class RequestHandlerController extends Controller #[PublicPage] #[NoCSRFRequired] #[BruteForceProtection(action: 'receiveFederatedShare')] - public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) - { + public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { try { // if request is signed and well signed, no exception are thrown // if request is not signed and host is known for not supporting signed request, no exception are thrown @@ -198,7 +195,7 @@ class RequestHandlerController extends Controller $share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType); $share->setProtocol($protocol); $provider->shareReceived($share); - } catch (ProviderDoesNotExistsException | ProviderCouldNotAddShareException $e) { + } catch (ProviderDoesNotExistsException|ProviderCouldNotAddShareException $e) { return new JSONResponse( ['message' => $e->getMessage()], Http::STATUS_NOT_IMPLEMENTED @@ -243,33 +240,26 @@ class RequestHandlerController extends Controller * @param string $email * @param string $name * @return JSONResponse - * 200: invitation accepted - * 400: Invalid token - * 403: Invitation token does not exist - * 409: User is allready known by the OCM provider - * spec link: https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post + * 200: invitation accepted + * 400: Invalid token + * 403: Invitation token does not exist + * 409: User is allready known by the OCM provider + * spec link: https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post */ #[PublicPage] #[NoCSRFRequired] #[BruteForceProtection(action: 'inviteAccepted')] - public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse - { + public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse { $this->logger->debug('Invite accepted for ' . $userId . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name); + $updated = new \DateTime('now'); + $updated = $updated->getTimeStamp(); /** @var IQueryBuilder $qb */ - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('federated_invites') - ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); - $result = $qb->executeQuery(); - $data = $result->fetch(); - $result->closeCursor(); - $found_for_this_user = false; - $updated = new DateTime("now"); - if ($data) { - $found_for_this_user = $data['recipient_user_id'] === $userId && isset($data['user_id']); + $invitation = null; + if($token) { + $invitation = $this->federatedInviteMapper->findByToken($token); } - if (!$found_for_this_user) { + if (!$invitation) { $response = ['message' => 'Invalid or non existing token', 'error' => true]; $status = Http::STATUS_BAD_REQUEST; $response = new JSONResponse($response, $status); @@ -283,46 +273,51 @@ class RequestHandlerController extends Controller } // Note: Not implementing 404 Invitation token does not exist, instead using 400 - if ($data['accepted'] === true) { + if ($invitation->getAccepted() === true) { $response = ['message' => 'Invite already accepted', 'error' => true]; $status = Http::STATUS_CONFLICT; return new JSONResponse($response, $status); } - if ($data['expiresAt'] < $updated) { + $unixstart = \DateTime::createFromFormat('U', '1'); + $expiredAt = \DateTime::createFromFormat('U', strval($invitation->getExpiredAt())); + if ($expiredAt == $unixstart) { + $invitation->setExpiredAt($updated); + } + if ($invitation->getExpiredAt() < $updated) { $response = ['message' => 'Invitation expired', 'error' => true]; $status = Http::STATUS_BAD_REQUEST; return new JSONResponse($response, $status); } - $localUser = $this->userManager->get($data['user_id']); + $localUser = $this->userManager->get($invitation->getUserId()); $sharedFromEmail = $localUser->getPrimaryEMailAddress(); $sharedFromDisplayName = $localUser->getDisplayName(); - $response = ['userID' => $data['user_id'], 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName]; + $response = ['userID' => $invitation->getUserId(), 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName]; $status = Http::STATUS_OK; - $qb->update('federated_invites') - ->set('accepted', $qb->createNamedParameter(true)) - ->set('acceptedAt', $qb->createNamedParameter($updated)) - ->set('recipient_email', $qb->createNamedParameter($email)) - ->set('recipient_name', $qb->createNamedParameter($name)) - ->set('recipient_user_id', $qb->createNamedParameter($userId)) - ->set('recipient_provider', $qb->createNamedParameter($recipientProvider)) - ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); - $qb->executeStatement(); - $invitation = new OCMInvitation( - accepted: true, - recipient_email: $email, - recipient_name: $name, - recipient_user_id: $userId, - recipient_provider: $recipientProvider, - token: $token, - user_id: $data['user_id'], - acceptedAt: $updated, - createdAt: $data['createdAt'], - expiresAt: $data['expiresAt'] - ); - $this->dispatcher->dispatchTyped(new OCMInvitationAcceptedEvent($invitation)); + $updated_data = [ + 'accepted' => true, + 'recipient_email' => $email, + 'recipient_name' => $name, + 'recipient_user_id' => $userId, + 'recipient_provider' => $recipientProvider, + 'token' => $token, + 'user_id' => $invitation->getUserId(), + 'acceptedAt' => $updated, + 'createdAt' => $invitation->getCreatedAt(), + 'expiredAt' => $invitation->getExpiredAt() + ]; + $invitation = $this->federatedInviteMapper->dataArrayToInvite($updated_data); + if (!$this->federatedInviteMapper->update($invitation)) { + $response = ['message' => 'Error updating invitation', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + return $response; + } + + $event = new FederatedInviteAcceptedEvent($invitation); + $this->dispatcher->dispatchTyped($event); return new JSONResponse($response, $status); } @@ -345,8 +340,7 @@ class RequestHandlerController extends Controller #[NoCSRFRequired] #[PublicPage] #[BruteForceProtection(action: 'receiveFederatedShareNotification')] - public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) - { + public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) { // check if all required parameters are set if ( $notificationType === null || @@ -425,8 +419,7 @@ class RequestHandlerController extends Controller * @param string $uid * @return string mixed */ - private function mapUid($uid) - { + private function mapUid($uid) { // FIXME this should be a method in the user management instead $this->logger->debug('shareWith before, ' . $uid, ['app' => $this->appName]); Util::emitHook( @@ -449,13 +442,12 @@ class RequestHandlerController extends Controller * @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request * @throws IncomingRequestException */ - private function getSignedRequest(): ?IIncomingSignedRequest - { + private function getSignedRequest(): ?IIncomingSignedRequest { try { $signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager); $this->logger->debug('signed request available', ['signedRequest' => $signedRequest]); return $signedRequest; - } catch (SignatureNotFoundException | SignatoryNotFoundException $e) { + } catch (SignatureNotFoundException|SignatoryNotFoundException $e) { $this->logger->debug('remote does not support signed request', ['exception' => $e]); // remote does not support signed request. // currently we still accept unsigned request until lazy appconfig @@ -485,8 +477,7 @@ class RequestHandlerController extends Controller * * @throws IncomingRequestException */ - private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void - { + private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void { if ($signedRequest === null) { $instance = $this->getHostFromFederationId($value); try { @@ -550,8 +541,7 @@ class RequestHandlerController extends Controller * @return void * @throws IncomingRequestException */ - private function confirmNotificationEntry(?IIncomingSignedRequest $signedRequest, string $entry): void - { + private function confirmNotificationEntry(?IIncomingSignedRequest $signedRequest, string $entry): void { $instance = $this->getHostFromFederationId($entry); if ($signedRequest === null) { try { @@ -570,8 +560,7 @@ class RequestHandlerController extends Controller * @return string * @throws IncomingRequestException */ - private function getHostFromFederationId(string $entry): string - { + private function getHostFromFederationId(string $entry): string { if (!str_contains($entry, '@')) { throw new IncomingRequestException('entry ' . $entry . ' does not contains @'); } diff --git a/apps/cloud_federation_api/lib/Db/FederatedInvite.php b/apps/cloud_federation_api/lib/Db/FederatedInvite.php new file mode 100644 index 00000000000..eb71c917853 --- /dev/null +++ b/apps/cloud_federation_api/lib/Db/FederatedInvite.php @@ -0,0 +1,93 @@ +addType('accepted', Types::BOOLEAN); + $this->addType('acceptedAt', Types::BIGINT); + $this->addType('createdAt', Types::BIGINT); + $this->addType('expiredAt', Types::BIGINT); + $this->addType('recipientEmail', Types::STRING); + $this->addType('recipientName', Types::STRING); + $this->addType('recipientProvider', Types::STRING); + $this->addType('recipientUserId', Types::STRING); + $this->addType('token', Types::STRING); + $this->addType('userId', Types::STRING); + } +} diff --git a/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php b/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php new file mode 100644 index 00000000000..3471e7c7a5c --- /dev/null +++ b/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php @@ -0,0 +1,40 @@ + + */ +class FederatedInviteMapper extends QBMapper { + public const TABLE_NAME = 'federated_invites'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::TABLE_NAME); + } + public function findByToken(string $token): ?FederatedInvite { + /** @var IQueryBuilder $qb */ + if (!$token) { + return null; + } + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('federated_invites') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); + return $this->findEntity($qb); + } + + public function dataArrayToInvite(array $data): FederatedInvite { + return $this->mapRowToEntity($data); + } +} diff --git a/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php b/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php new file mode 100644 index 00000000000..9dd33c9c9a1 --- /dev/null +++ b/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\CloudFederationAPI\Events; + +use OCA\CloudFederationApi\Db\FederatedInvite; +use OCP\EventDispatcher\Event; + +class FederatedInviteAcceptedEvent extends Event { + public function __construct( + private FederatedInvite $invitation, + ) { + parent::__construct(); + } + + public function getInvitation(): FederatedInvite { + return $this->invitation; + } +} diff --git a/apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php b/apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php deleted file mode 100644 index d1ebcdaf7d8..00000000000 --- a/apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php +++ /dev/null @@ -1,26 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-only - */ - -namespace OCA\CloudFederationApi\Events; - -use OCP\EventDispatcher\Event; -use OCA\CloudFederationApi\OCMInvitation; - -class OCMInvitationAcceptedEvent extends Event -{ - public function __construct( - private OCMInvitation $invitation - ) { - parent::__construct(); - } - - public function getInvitation(): OCMInvitation - { - return $this->invitation; - } -} diff --git a/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php index ab4bf627c14..c1c0ebed132 100644 --- a/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php +++ b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php @@ -15,8 +15,7 @@ use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; -class Version1016Date202502262004 extends SimpleMigrationStep -{ +class Version1016Date202502262004 extends SimpleMigrationStep { /** * @param IOutput $output @@ -24,8 +23,7 @@ class Version1016Date202502262004 extends SimpleMigrationStep * @param array $options * @return null|ISchemaWrapper */ - public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) - { + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { /** @var ISchemaWrapper $schema */ @@ -75,22 +73,24 @@ class Version1016Date202502262004 extends SimpleMigrationStep 'notnull' => false, 'default' => false ]); - $table->addColumn('createdAt', Types::DATETIME, [ + $table->addColumn('created_at', Types::BIGINT, [ 'notnull' => true, ]); - $table->addColumn('expiredAt', Types::DATETIME, [ + $table->addColumn('expired_at', Types::BIGINT, [ 'notnull' => false, ]); - $table->addColumn('acceptedAt', Types::DATETIME, [ + $table->addColumn('accepted_at', Types::BIGINT, [ 'notnull' => false, ]); $table->setPrimaryKey(['id']); + return $schema; } - return $schema; + return null; + } } diff --git a/apps/cloud_federation_api/lib/OCMInvitation.php b/apps/cloud_federation_api/lib/OCMInvitation.php deleted file mode 100644 index ac3dfe3ca53..00000000000 --- a/apps/cloud_federation_api/lib/OCMInvitation.php +++ /dev/null @@ -1,18 +0,0 @@ - array($baseDir . '/lib/private'), 'OCP\\' => array($baseDir . '/lib/public'), 'NCU\\' => array($baseDir . '/lib/unstable'), + 'Bamarni\\Composer\\Bin\\' => array($vendorDir . '/bamarni/composer-bin-plugin/src'), '' => array($baseDir . '/lib/private/legacy'), ); diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 7a38da17c82..956fa820ac5 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -21,6 +21,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 array ( 'NCU\\' => 4, ), + 'B' => + array ( + 'Bamarni\\Composer\\Bin\\' => 21, + ), ); public static $prefixDirsPsr4 = array ( @@ -40,6 +44,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 array ( 0 => __DIR__ . '/../../..' . '/lib/unstable', ), + 'Bamarni\\Composer\\Bin\\' => + array ( + 0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src', + ), ); public static $fallbackDirsPsr4 = array ( diff --git a/lib/public/OCM/IOCMProvider.php b/lib/public/OCM/IOCMProvider.php index ea5bdcf3362..883c68a30ae 100644 --- a/lib/public/OCM/IOCMProvider.php +++ b/lib/public/OCM/IOCMProvider.php @@ -58,7 +58,7 @@ interface IOCMProvider extends JsonSerializable { * returns the capabilities of the API * * @return array - * @since 30.0.0 + * @since 32.0.0 */ public function getCapabilities(): array; @@ -68,7 +68,7 @@ interface IOCMProvider extends JsonSerializable { * @param array $capabilities * * @return $this - * @since 30.0.0 + * @since 32.0.0 */ public function setCapabilities(array $capabilities): static; @@ -95,7 +95,7 @@ interface IOCMProvider extends JsonSerializable { * get provider * * @return string - * @since 30.0.0 + * @since 32.0.0 */ public function getProvider(): string; /**