diff --git a/public/typo3conf/ext/ep_products/Classes/Controller/NewsletterController.php b/public/typo3conf/ext/ep_products/Classes/Controller/NewsletterController.php index e92002f5..07c4ae43 100644 --- a/public/typo3conf/ext/ep_products/Classes/Controller/NewsletterController.php +++ b/public/typo3conf/ext/ep_products/Classes/Controller/NewsletterController.php @@ -27,15 +27,54 @@ namespace EP\EpProducts\Controller; * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use EP\EpProducts\Domain\Model\Dto\NewsletterSubscriptionRequest; +use EP\EpProducts\MyEP\ApiClient; +use EP\EpProducts\MyEP\ApiException; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; class NewsletterController extends ActionController { + private ApiClient $apiClient; + + public function __construct(ApiClient $apiClient) + { + $this->apiClient = $apiClient; + } public function indexAction() { $this->view->assign('result', GeneralUtility::_GET('result')); } + public function subscriptionFormAction(?NewsletterSubscriptionRequest $subscriptionRequest = null) + { + if (null === $subscriptionRequest) { + $subscriptionRequest = new NewsletterSubscriptionRequest(); + } + + try { + $lists = $this->apiClient->getNewsletterLists(); + } catch (ApiException $e) { + $lists = []; + } + + $this->view->assign('subscriptionRequest', $subscriptionRequest); + $this->view->assign('lists', $lists); + } + + public function subscriptionFormSubmitAction(NewsletterSubscriptionRequest $subscriptionRequest) + { + try { + $result = $this->apiClient->subscribeToNewsletters($subscriptionRequest); + $result['success'] = true; + } catch (ApiException $e) { + $result = [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + } + + $this->view->assign('result', $result); + } } diff --git a/public/typo3conf/ext/ep_products/Classes/Domain/Model/Dto/NewsletterSubscriptionRequest.php b/public/typo3conf/ext/ep_products/Classes/Domain/Model/Dto/NewsletterSubscriptionRequest.php new file mode 100644 index 00000000..2a976ce2 --- /dev/null +++ b/public/typo3conf/ext/ep_products/Classes/Domain/Model/Dto/NewsletterSubscriptionRequest.php @@ -0,0 +1,113 @@ + + * @TYPO3\CMS\Extbase\Annotation\Validate("NotEmpty") + */ + private array $listIds = []; + + /** + * @TYPO3\CMS\Extbase\Annotation\Validate("Boolean", options={"is": true}) + */ + private bool $consent = false; + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(?string $firstName): self + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(?string $lastName): self + { + $this->lastName = $lastName; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(?string $email): self + { + $this->email = $email; + + return $this; + } + + /** + * @return list + */ + public function getListIds(): array + { + return $this->listIds; + } + + /** + * @param list $listIds + */ + public function setListIds(array $listIds): self + { + $this->listIds = $listIds; + + return $this; + } + + public function isConsent(): bool + { + return $this->consent; + } + + public function setConsent(bool $consent): self + { + $this->consent = $consent; + + return $this; + } + + public function jsonSerialize(): array + { + return [ + 'firstName' => $this->firstName, + 'lastName' => $this->lastName, + 'email' => $this->email, + 'listIds' => array_map(function($value) { + return (int) $value; + }, $this->listIds), + ]; + } +} diff --git a/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php index 02253227..6a10d1ed 100644 --- a/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php +++ b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php @@ -2,16 +2,20 @@ namespace EP\EpProducts\MyEP; +use EP\EpProducts\Domain\Model\Dto\NewsletterSubscriptionRequest; +use GuzzleHttp\Exception\GuzzleException; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\GenericProvider; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; -use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpClient\HttpClient; -use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -26,30 +30,18 @@ class ApiClient implements LoggerAwareInterface */ public function getLastUpdateAt(): ?\DateTimeImmutable { - $httpClient = $this->getHttpClient(); + $data = $this->requestJson('GET', 'last-update'); - try { - $response = $httpClient->request('GET', 'last-update'); - } catch (TransportExceptionInterface $e) { - $this->logger->error($e->getMessage()); - throw new ApiException($e->getMessage()); + if (!isset($data['timestamp']) || null === $data['timestamp'] || '' === $data['timestamp']) { + return null; } try { - if (200 !== $response->getStatusCode()) { - throw new ApiException($response->getStatusCode()); - } - - $data = $response->toArray(false); - - if (null === $data['timestamp'] ?? null) { - return null; - } - return new \DateTimeImmutable($data['timestamp']); } catch (\Throwable $e) { - $this->logger->error($e->getMessage()); - throw new ApiException($e->getMessage()); + $message = sprintf('Invalid timestamp returned by MyEP API for GET last-update: %s', $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); } } @@ -58,25 +50,7 @@ class ApiClient implements LoggerAwareInterface */ public function getPickups(): array { - $httpClient = $this->getHttpClient(); - - try { - $response = $httpClient->request('GET', 'pickups'); - } catch (TransportExceptionInterface $e) { - $this->logger->error($e->getMessage()); - throw new ApiException($e->getMessage()); - } - - try { - if (200 !== $response->getStatusCode()) { - throw new ApiException($response->getStatusCode()); - } - - return $response->toArray(false); - } catch (\Throwable $e) { - $this->logger->error($e->getMessage()); - throw new ApiException($e->getMessage()); - } + return $this->requestJson('GET', 'pickups'); } /** @@ -84,25 +58,7 @@ class ApiClient implements LoggerAwareInterface */ public function getTravels(): array { - $httpClient = $this->getHttpClient(); - - try { - $response = $httpClient->request('GET', 'travels'); - } catch (TransportExceptionInterface $e) { - $this->logger->error($e->getMessage()); - throw new ApiException($e->getMessage()); - } - - try { - if (200 !== $response->getStatusCode()) { - throw new ApiException($response->getStatusCode()); - } - - return $response->toArray(false); - } catch (\Throwable $e) { - $this->logger->error($e->getMessage()); - throw new ApiException($e->getMessage()); - } + return $this->requestJson('GET', 'travels'); } /** @@ -110,18 +66,33 @@ class ApiClient implements LoggerAwareInterface */ public function registerAddress(array $addressData): void { - $httpClient = $this->getHttpClient(); + $response = $this->request('POST', 'contactform', [ + 'json' => $addressData, + ]); - try { - $httpClient->request('POST', 'contactform', [ - 'json' => $addressData, - ]); - } catch (ClientException $e) { - $this->logger->warning($e->getMessage()); - } catch (TransportExceptionInterface $e) { - $this->logger->error($e->getMessage()); - throw new ApiException($e->getMessage()); - } + $this->assertStatusCode($response, [Response::HTTP_OK, Response::HTTP_CREATED, Response::HTTP_ACCEPTED, Response::HTTP_NO_CONTENT], 'POST', 'contactform'); + } + + /** + * @throws ApiException + */ + public function getNewsletterLists(): array + { + return $this->requestJson('GET', 'newsletters'); + } + + /** + * @throws ApiException + */ + public function subscribeToNewsletters(NewsletterSubscriptionRequest $subscriptionRequest): array + { + $response = $this->request('POST', 'newsletter-subscriptions', [ + 'json' => $subscriptionRequest, + ]); + + $this->assertStatusCode($response, [Response::HTTP_OK, Response::HTTP_ACCEPTED], 'POST', 'newsletter-subscriptions'); + + return $this->decodeResponse($response, 'POST', 'newsletter-subscriptions'); } /** @@ -129,23 +100,7 @@ class ApiClient implements LoggerAwareInterface */ public function getTravel(string $productCode): array { - $httpClient = $this->getHttpClient(); - - try { - $response = $httpClient->request('GET', 'travels/' . $productCode); - } catch (TransportExceptionInterface $e) { - throw new ApiException($e->getMessage()); - } - - try { - if (200 !== $response->getStatusCode()) { - throw new ApiException($response->getStatusCode()); - } - - return $response->toArray(false); - } catch (\Throwable $e) { - throw new ApiException($e->getMessage()); - } + return $this->requestJson('GET', 'travels/' . $productCode); } /** @@ -153,22 +108,75 @@ class ApiClient implements LoggerAwareInterface */ public function getPickupsPlanning(string $travelCode): array { - $httpClient = $this->getHttpClient(); + return $this->requestJson('GET', 'pickups-planning/' . $travelCode); + } + /** + * @throws ApiException + */ + private function requestJson(string $method, string $uri, array $options = [], array $expectedStatusCodes = [Response::HTTP_OK]): array + { + $response = $this->request($method, $uri, $options); + + $this->assertStatusCode($response, $expectedStatusCodes, $method, $uri); + + return $this->decodeResponse($response, $method, $uri); + } + + /** + * @throws ApiException + */ + private function request(string $method, string $uri, array $options = []): ResponseInterface + { try { - $response = $httpClient->request('GET', 'pickups-planning/' . $travelCode); + return $this->getHttpClient()->request($method, $uri, $options); } catch (TransportExceptionInterface $e) { - throw new ApiException($e->getMessage()); + $message = sprintf('MyEP API transport error for %s %s: %s', $method, $uri, $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); + } + } + + /** + * @param int[] $expectedStatusCodes + * + * @throws ApiException + */ + private function assertStatusCode(ResponseInterface $response, array $expectedStatusCodes, string $method, string $uri): void + { + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + $message = sprintf('MyEP API transport error while reading status for %s %s: %s', $method, $uri, $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); } - try { - if (200 !== $response->getStatusCode()) { - throw new ApiException($response->getStatusCode()); - } + if (true === in_array($statusCode, $expectedStatusCodes, true)) { + return; + } + $message = sprintf('MyEP API returned unexpected status %d for %s %s', $statusCode, $method, $uri); + $this->logWarning($message); + + throw new ApiException($message, $statusCode); + } + + /** + * @throws ApiException + */ + private function decodeResponse(ResponseInterface $response, string $method, string $uri): array + { + try { return $response->toArray(false); - } catch (\Throwable $e) { - throw new ApiException($e->getMessage()); + } catch (DecodingExceptionInterface $e) { + $message = sprintf('MyEP API returned invalid JSON for %s %s: %s', $method, $uri, $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); + } catch (TransportExceptionInterface $e) { + $message = sprintf('MyEP API transport error while reading body for %s %s: %s', $method, $uri, $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); } } @@ -177,18 +185,41 @@ class ApiClient implements LoggerAwareInterface $config = GeneralUtility::makeInstance(ExtensionConfiguration::class) ->get('ep_products'); - if (null === static::$accessToken || true === static::$accessToken->hasExpired()) { - static::$accessToken = $this - ->getProvider($config) - ->getAccessToken('client_credentials') - ; - } + static::$accessToken = $this->getAccessToken($config); return HttpClient::createForBaseUri($config['myEpApiBaseUrl'], [ 'auth_bearer' => static::$accessToken->getToken(), ]); } + /** + * @throws ApiException + */ + private function getAccessToken(array $config): AccessToken + { + if (null !== static::$accessToken && false === static::$accessToken->hasExpired()) { + return static::$accessToken; + } + + try { + static::$accessToken = $this->getProvider($config)->getAccessToken('client_credentials'); + } catch (IdentityProviderException $e) { + $message = sprintf('MyEP OAuth token request failed: %s', $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); + } catch (\UnexpectedValueException $e) { + $message = sprintf('MyEP OAuth token response was invalid: %s', $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); + } catch (GuzzleException $e) { + $message = sprintf('MyEP OAuth token transport failed: %s', $e->getMessage()); + $this->logError($message, ['exception' => $e]); + throw new ApiException($message, 0, $e); + } + + return static::$accessToken; + } + private function getProvider(array $config): AbstractProvider { return new GenericProvider([ @@ -201,4 +232,18 @@ class ApiClient implements LoggerAwareInterface 'scopes' => 'api', ]); } + + private function logError(string $message, array $context = []): void + { + if (null !== $this->logger) { + $this->logger->error($message, $context); + } + } + + private function logWarning(string $message, array $context = []): void + { + if (null !== $this->logger) { + $this->logger->warning($message, $context); + } + } } diff --git a/public/typo3conf/ext/ep_products/Configuration/TCA/Overrides/tt_content.php b/public/typo3conf/ext/ep_products/Configuration/TCA/Overrides/tt_content.php index 895bbf25..45e7dcb5 100644 --- a/public/typo3conf/ext/ep_products/Configuration/TCA/Overrides/tt_content.php +++ b/public/typo3conf/ext/ep_products/Configuration/TCA/Overrides/tt_content.php @@ -131,6 +131,12 @@ call_user_func(function () { 'Newsletter' ); + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( + 'EP.EpProducts', + 'newsletter_subscription', + 'Newsletter-/RA-Registrierung' + ); + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( 'EP.EpProducts', 'maps', diff --git a/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript b/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript index 7eecbe49..0c65137a 100644 --- a/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript +++ b/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript @@ -57,6 +57,7 @@ plugin.tx_epproducts { logoImage = {$plugin.tx_eptheme.settings.logoImage} themekey = {$plugin.tx_eptheme.settings.themekey} globalConceptCode = {$plugin.tx_eptheme.settings.globalConceptCode} + dataProtectionPageUid = {$plugin.tx_eptheme.settings.dataProtectionPageUid} datePickerPresets { 1 { label = Silvester diff --git a/public/typo3conf/ext/ep_products/ext_localconf.php b/public/typo3conf/ext/ep_products/ext_localconf.php index 7c65103b..0687171e 100644 --- a/public/typo3conf/ext/ep_products/ext_localconf.php +++ b/public/typo3conf/ext/ep_products/ext_localconf.php @@ -213,6 +213,17 @@ $boot = function () { ] ); + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( + 'EP.ep_products', + 'newsletter_subscription', + [ + 'Newsletter' => 'subscriptionForm,subscriptionFormSubmit', + ], + [ + 'Newsletter' => 'subscriptionForm,subscriptionFormSubmit', + ] + ); + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( 'EP.ep_products', 'maps', diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Language/de.locallang.xlf b/public/typo3conf/ext/ep_theme/Resources/Private/Language/de.locallang.xlf index 958fee3d..5ecbc1e8 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Language/de.locallang.xlf +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Language/de.locallang.xlf @@ -85,6 +85,9 @@ Bitte angeben + + Bitte angeben + Bitte angeben @@ -92,7 +95,14 @@ Bitte angeben - Ungültige E-Mail Adresse + Bitte gib eine gültige E-Mail Adresse an + + + + Bitte wähle mindestens einen Newsletter + + + Bitte stimme den Datenschutzbestimmungen zu diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Newsletter/SubscriptionForm.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Newsletter/SubscriptionForm.html new file mode 100644 index 00000000..9006e7be --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Newsletter/SubscriptionForm.html @@ -0,0 +1,91 @@ + + + + + + +
+ +
+
+

+ Meine Daten +

+ + + +
+ + + + + + +
+ +
+
+
+

+ Ich möchte folgende Newsletter erhalten +

+ + + + + + + + +
+
+
+ +
+
+
+
+
+ + + + + + + + + diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Newsletter/SubscriptionFormSubmit.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Newsletter/SubscriptionFormSubmit.html new file mode 100644 index 00000000..3e55f692 --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Newsletter/SubscriptionFormSubmit.html @@ -0,0 +1,23 @@ + + + + + +
+
+

+ Vielen Dank für dein Interesse +

+

+ Deine Anmeldung muss noch bestätigt werden. Bitte nutze den Link aus der Bestätigungs-E-Mail, die in + Kürze bei dir eintreffen sollte. +

+

+ Sieh am besten auch im Spam-Ordner nach. +

+
+
+
+ +