From da4386053b71210b135cb1aeb94ebfdf0582ddee Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 11 Nov 2025 10:02:11 +0100 Subject: [PATCH 1/2] update extcode/cart-paypal to work with TYPO3 v13.x but with the old PayPal AIP --- .gitignore | 16 +- .../Controller/Order/PaymentController.php | 732 +++++++++--------- Classes/Event/Order/CancelEvent.php | 140 ++-- Classes/Event/Order/NotifyEvent.php | 140 ++-- Classes/Event/Order/SuccessEvent.php | 140 ++-- .../EventListener/Order/Payment/ClearCart.php | 66 +- .../Order/Payment/ProviderRedirect.php | 538 ++++++------- .../ExtcodeCartPaypalCTypeMigration.php | 35 + Configuration/Services.yaml | 14 +- Configuration/TCA/Overrides/sys_template.php | 24 +- Configuration/TCA/Overrides/tt_content.php | 9 + ...tx_cart_domain_model_order_transaction.php | 48 +- Configuration/TypoScript/constants.typoscript | 2 +- Configuration/TypoScript/setup.typoscript | 47 +- Documentation/Includes.txt | 40 +- LICENSE | 678 ++++++++-------- Resources/Private/Layouts/Default.html | 4 +- .../Templates/Order/Payment/Cancel.html | 22 +- .../Templates/Order/Payment/Confirm.html | 28 +- .../Templates/Order/Payment/Notify.html | 22 +- .../Templates/Order/Payment/Success.html | 28 +- Resources/Public/Icons/Extension.svg | 32 +- composer.json | 179 +++-- ext_emconf.php | 49 +- ext_localconf.php | 33 +- rector.php | 230 +++--- 26 files changed, 1650 insertions(+), 1646 deletions(-) create mode 100644 Classes/Updates/ExtcodeCartPaypalCTypeMigration.php create mode 100644 Configuration/TCA/Overrides/tt_content.php diff --git a/.gitignore b/.gitignore index bbf6d53..be04259 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -.build -composer.lock - -.php_cs.cache -.phplint.cache - -.DS_Store -.idea +.build +composer.lock + +.php_cs.cache +.phplint.cache + +.DS_Store +.idea diff --git a/Classes/Controller/Order/PaymentController.php b/Classes/Controller/Order/PaymentController.php index 7c57bde..4bacb0d 100644 --- a/Classes/Controller/Order/PaymentController.php +++ b/Classes/Controller/Order/PaymentController.php @@ -1,353 +1,379 @@ -logger = $logManager->getLogger(); - $this->persistenceManager = $persistenceManager; - $this->sessionHandler = $sessionHandler; - $this->cartRepository = $cartRepository; - $this->paymentRepository = $paymentRepository; - } - - protected function initializeAction(): void - { - $this->cartConf = - $this->configurationManager->getConfiguration( - ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, - 'Cart' - ); - - $this->cartPaypalConf = - $this->configurationManager->getConfiguration( - ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, - 'CartPayPal' - ); - } - - public function successAction(): void - { - if ($this->request->hasArgument('hash') && !empty($this->request->getArgument('hash'))) { - $this->loadCartByHash($this->request->getArgument('hash')); - - if ($this->cart) { - $orderItem = $this->cart->getOrderItem(); - - $successEvent = new SuccessEvent($this->cart->getCart(), $orderItem, $this->cartConf); - $this->eventDispatcher->dispatch($successEvent); - - $this->redirect('show', 'Cart\Order', 'Cart', ['orderItem' => $orderItem]); - } else { - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.success.error_occured', - 'cart_paypal' - ), - '', - AbstractMessage::ERROR - ); - } - } else { - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.success.access_denied', - 'cart_paypal' - ), - '', - AbstractMessage::ERROR - ); - } - } - - public function cancelAction(): void - { - if ($this->request->hasArgument('hash') && !empty($this->request->getArgument('hash'))) { - $this->loadCartByHash($this->request->getArgument('hash'), 'FHash'); - - if ($this->cart) { - $orderItem = $this->cart->getOrderItem(); - $payment = $orderItem->getPayment(); - - $this->restoreCartSession(); - - $payment->setStatus('canceled'); - - $this->paymentRepository->update($payment); - $this->persistenceManager->persistAll(); - - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.cancel.successfully_canceled', - 'cart_paypal' - ) - ); - - $cancelEvent = new CancelEvent($this->cart->getCart(), $orderItem, $this->cartConf); - $this->eventDispatcher->dispatch($cancelEvent); - - $this->redirect('show', 'Cart\Cart', 'Cart'); - } else { - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.cancel.error_occured', - 'cart_paypal' - ), - '', - AbstractMessage::ERROR - ); - } - } else { - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.cancel.access_denied', - 'cart_paypal' - ), - '', - AbstractMessage::ERROR - ); - } - } - - public function notifyAction() - { - if ($this->request->getMethod() !== 'POST') { - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(405); - exit(); - } - return $this->htmlResponse()->withStatus(405, 'Method not allowed.'); - } - - $postData = GeneralUtility::_POST(); - - $curlRequest = $this->getCurlRequestFromPostData($postData); - - if ($this->cartPaypalConf['debug']) { - $this->logger->debug( - 'Log Data', - [ - '$parsedPostData' => $postData, - '$curlRequest' => $curlRequest - ] - ); - } - - $this->execCurlRequest($curlRequest); - - $cartSHash = $postData['custom']; - if (empty($cartSHash)) { - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(403); - exit(); - } - return $this->htmlResponse()->withStatus(403, 'Not allowed.'); - } - - $this->loadCartByHash($this->request->getArgument('hash')); - - if ($this->cart === null) { - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(404); - exit(); - } - return $this->htmlResponse()->withStatus(404, 'Page / Cart not found.'); - } - - $orderItem = $this->cart->getOrderItem(); - $payment = $orderItem->getPayment(); - - if ($payment->getStatus() !== 'paid') { - $payment->setStatus('paid'); - $this->paymentRepository->update($payment); - $this->persistenceManager->persistAll(); - - $notifyEvent = new NotifyEvent($this->cart->getCart(), $orderItem, $this->cartConf); - $this->eventDispatcher->dispatch($notifyEvent); - } - - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(200); - exit(); - } - return $this->htmlResponse()->withStatus(200); - } - - protected function restoreCartSession(): void - { - $cart = $this->cart->getCart(); - $cart->resetOrderNumber(); - $cart->resetInvoiceNumber(); - $this->sessionHandler->write($cart, $this->cartConf['settings']['cart']['pid']); - } - - protected function getCurlRequestFromPostData(array $parsePostData): string - { - $curlRequest = 'cmd=_notify-validate'; - foreach ($parsePostData as $key => $value) { - $value = urlencode($value); - $curlRequest .= "&$key=$value"; - } - - return $curlRequest; - } - - protected function execCurlRequest(string $curlRequest): bool - { - $paypalUrl = $this->getPaypalUrl(); - - $ch = curl_init($paypalUrl); - if ($ch === false) { - return false; - } - - curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $curlRequest); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); - - if (is_array($this->cartPaypalConf) && intval($this->cartPaypalConf['curl_timeout'])) { - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, intval($this->cartPaypalConf['curl_timeout'])); - } else { - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 300); - } - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); - - $curlResult = strtolower(curl_exec($ch)); - $curlError = curl_errno($ch); - - if ($curlError !== 0) { - $this->logger->warning( - 'paypal-payment-api', - [ - 'ERROR' => 'Can\'t connect to PayPal to validate IPN message', - 'curl_error' => curl_error($ch), - 'curl_request' => $curlRequest, - 'curl_result' => $curlResult, - ] - ); - - curl_close($ch); - exit; - } - - if ($this->cartPaypalConf['debug']) { - $this->logger->debug( - 'paypal-payment-api', - [ - 'curl_info' => curl_getinfo($ch, CURLINFO_HEADER_OUT), - 'curl_request' => $curlRequest, - 'curl_result' => $curlResult, - ] - ); - } - - $curlResults = explode("\r\n\r\n", $curlResult); - - curl_close($ch); - - return true; - } - - protected function getPaypalUrl(): string - { - if ($this->cartPaypalConf['sandbox']) { - return self::PAYPAL_API_SANDBOX; - } - - return self::PAYPAL_API_LIVE; - } - - protected function loadCartByHash(string $hash, string $type = 'SHash'): void - { - $querySettings = GeneralUtility::makeInstance( - Typo3QuerySettings::class - ); - $querySettings->setStoragePageIds([$this->cartConf['settings']['order']['pid']]); - $this->cartRepository->setDefaultQuerySettings($querySettings); - - $findOneByMethod = 'findOneBy' . $type; - $this->cart = $this->cartRepository->$findOneByMethod($hash); - } -} +logger = $logManager->getLogger(); + $this->persistenceManager = $persistenceManager; + $this->sessionHandler = $sessionHandler; + $this->cartRepository = $cartRepository; + $this->paymentRepository = $paymentRepository; + } + + protected function initializeAction(): void + { + $this->cartConf = + $this->configurationManager->getConfiguration( + ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, + 'Cart' + ); + + $this->cartPaypalConf = + $this->configurationManager->getConfiguration( + ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, + 'CartPayPal' + ); + } + + public function successAction(): ResponseInterface + { + if ($this->request->hasArgument('hash') && !empty($this->request->getArgument('hash'))) { + $this->loadCartByHash($this->request->getArgument('hash')); + + if ($this->cart) { + $orderItem = $this->cart->getOrderItem(); + + $successEvent = new SuccessEvent($this->cart->getCart(), $orderItem, $this->cartConf); + $this->eventDispatcher->dispatch($successEvent); + + return $this->redirect('show', 'Cart\Order', 'Cart', ['orderItem' => $orderItem]); + } else { + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.success.error_occured', + 'CartPaypal' + ), + '', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + } + } else { + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.success.access_denied', + 'CartPaypal' + ), + '', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + } + } + + public function cancelAction(): ResponseInterface + { + if ($this->request->hasArgument('hash') && !empty($this->request->getArgument('hash'))) { + $this->loadCartByHash($this->request->getArgument('hash'), 'FHash'); + + if ($this->cart) { + $orderItem = $this->cart->getOrderItem(); + $payment = $orderItem->getPayment(); + + $this->restoreCartSession(); + + $payment->setStatus('canceled'); + + $this->paymentRepository->update($payment); + $this->persistenceManager->persistAll(); + + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.cancel.successfully_canceled', + 'CartPaypal' + ) + ); + + $cancelEvent = new CancelEvent($this->cart->getCart(), $orderItem, $this->cartConf); + $this->eventDispatcher->dispatch($cancelEvent); + + return $this->redirect('show', 'Cart\Cart', 'Cart'); + } else { + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.cancel.error_occured', + 'CartPaypal' + ), + '', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + } + } else { + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.cancel.access_denied', + 'CartPaypal' + ), + '', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + } + } + + public function notifyAction(): \Psr\Http\Message\ResponseInterface + { + + if ($this->request->getMethod() !== 'POST') { + // exit with Status Code in TYPO3 v10.4 + if (isset($this->response)) { + $this->response->setStatus(405); + exit(); + } + return $this->htmlResponse()->withStatus(405, 'Method not allowed.'); + } + + $postData = $this->request->getParsedBody(); + $curlRequest = $this->getCurlRequestFromPostData($postData); + + if ( + is_array($this->cartPaypalConf) && + array_key_exists('debug', $this->cartPaypalConf) && + $this->cartPaypalConf['debug'] + ) { + $this->logger->debug( + 'Log Data', + [ + '$parsedPostData' => $postData, + '$curlRequest' => $curlRequest + ] + ); + } + + $this->execCurlRequest($curlRequest); + $cartSHash = $postData['custom']; + if (empty($cartSHash)) { + // exit with Status Code in TYPO3 v10.4 + if (isset($this->response)) { + $this->response->setStatus(403); + exit(); + } + return $this->htmlResponse()->withStatus(403, 'Not allowed.'); + } + + $this->loadCartByHash($this->request->getArgument('hash')); + + if ($this->cart === null) { + // exit with Status Code in TYPO3 v10.4 + if (isset($this->response)) { + $this->response->setStatus(404); + exit(); + } + return $this->htmlResponse()->withStatus(404, 'Page / Cart not found.'); + } + + $orderItem = $this->cart->getOrderItem(); + $payment = $orderItem->getPayment(); + + if ($payment->getStatus() !== 'paid' && $postData['payment_status'] == 'Completed') { + $payment->setStatus('paid'); + $this->paymentRepository->update($payment); + $this->persistenceManager->persistAll(); + + $notifyEvent = new NotifyEvent($this->cart->getCart(), $orderItem, $this->cartConf); + $this->eventDispatcher->dispatch($notifyEvent); + } + + // exit with Status Code in TYPO3 v10.4 + if (isset($this->response)) { + $this->response->setStatus(200); + exit(); + } + return $this->htmlResponse()->withStatus(200); + } + + protected function restoreCartSession(): void + { + $cart = $this->cart->getCart(); + $cart->resetOrderNumber(); + $cart->resetInvoiceNumber(); + + // Neue Methode ab TYPO3 10+/Cart 8+: + #$this->sessionHandler->storeCart($cart->getCart(), $this->cartConf['settings']['cart']['pid']); + $this->sessionHandler->writeCart((string)$this->cartConf['settings']['cart']['pid'], $cart); + + + // Oder alternativ für ältere Versionen: + // $this->sessionHandler->store($cart, $this->cartConf['settings']['cart']['pid']); + } + + protected function getCurlRequestFromPostData(array $parsePostData): string + { + $curlRequest = 'cmd=_notify-validate'; + foreach ($parsePostData as $key => $value) { + $value = urlencode($value); + $curlRequest .= "&$key=$value"; + } + + return $curlRequest; + } + + protected function execCurlRequest(string $curlRequest): bool + { + $paypalUrl = $this->getPaypalUrl(); + + $ch = curl_init($paypalUrl); + if ($ch === false) { + return false; + } + + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $curlRequest); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); + + if ( + is_array($this->cartPaypalConf) + && array_key_exists('curl_timeout', $this->cartPaypalConf) + && intval($this->cartPaypalConf['curl_timeout']) + ) { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, intval($this->cartPaypalConf['curl_timeout'])); + } else { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 300); + } + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); + + $curlResult = strtolower(curl_exec($ch)); + $curlError = curl_errno($ch); + + if ($curlError !== 0) { + $this->logger->warning( + 'paypal-payment-api', + [ + 'ERROR' => 'Can\'t connect to PayPal to validate IPN message', + 'curl_error' => curl_error($ch), + 'curl_request' => $curlRequest, + 'curl_result' => $curlResult, + ] + ); + + curl_close($ch); + exit; + } + + if ( + is_array($this->cartPaypalConf) && + array_key_exists('debug', $this->cartPaypalConf) && + $this->cartPaypalConf['debug'] + ) { + $this->logger->debug( + 'paypal-payment-api', + [ + 'curl_info' => curl_getinfo($ch, CURLINFO_HEADER_OUT), + 'curl_request' => $curlRequest, + 'curl_result' => $curlResult, + ] + ); + } + + $curlResults = explode("\r\n\r\n", $curlResult); + + curl_close($ch); + + return true; + } + + protected function getPaypalUrl(): string + { + if ($this->cartPaypalConf['sandbox']) { + return self::PAYPAL_API_SANDBOX; + } + + return self::PAYPAL_API_LIVE; + } + + protected function loadCartByHash(string $hash, string $type = 'SHash'): void + { + $querySettings = GeneralUtility::makeInstance( + Typo3QuerySettings::class + ); + $querySettings->setStoragePageIds([$this->cartConf['settings']['order']['pid']]); + $this->cartRepository->setDefaultQuerySettings($querySettings); + + $findOneByMethod = 'findOneBy' . $type; + #$this->cart = $this->cartRepository->findOneBy(['method' => $hash]); + if($type == 'FHash') { + $this->cart = $this->cartRepository->findOneByFHash($hash); + } + else { + $this->cart = $this->cartRepository->findOneBySHash($hash); + } + + } +} diff --git a/Classes/Event/Order/CancelEvent.php b/Classes/Event/Order/CancelEvent.php index 2f2bd68..79c6c90 100644 --- a/Classes/Event/Order/CancelEvent.php +++ b/Classes/Event/Order/CancelEvent.php @@ -1,70 +1,70 @@ -cart = $cart; - $this->orderItem = $orderItem; - $this->settings = $settings; - } - - public function getCart(): Cart - { - return $this->cart; - } - - public function getOrderItem(): OrderItem - { - return $this->orderItem; - } - - public function getSettings(): array - { - return $this->settings; - } - - public function setPropagationStopped(bool $isPropagationStopped): void - { - $this->isPropagationStopped = $isPropagationStopped; - } - - public function isPropagationStopped(): bool - { - return $this->isPropagationStopped; - } -} +cart = $cart; + $this->orderItem = $orderItem; + $this->settings = $settings; + } + + public function getCart(): Cart + { + return $this->cart; + } + + public function getOrderItem(): OrderItem + { + return $this->orderItem; + } + + public function getSettings(): array + { + return $this->settings; + } + + public function setPropagationStopped(bool $isPropagationStopped): void + { + $this->isPropagationStopped = $isPropagationStopped; + } + + public function isPropagationStopped(): bool + { + return $this->isPropagationStopped; + } +} diff --git a/Classes/Event/Order/NotifyEvent.php b/Classes/Event/Order/NotifyEvent.php index 17c0c53..4771c22 100644 --- a/Classes/Event/Order/NotifyEvent.php +++ b/Classes/Event/Order/NotifyEvent.php @@ -1,70 +1,70 @@ -cart = $cart; - $this->orderItem = $orderItem; - $this->settings = $settings; - } - - public function getCart(): Cart - { - return $this->cart; - } - - public function getOrderItem(): OrderItem - { - return $this->orderItem; - } - - public function getSettings(): array - { - return $this->settings; - } - - public function setPropagationStopped(bool $isPropagationStopped): void - { - $this->isPropagationStopped = $isPropagationStopped; - } - - public function isPropagationStopped(): bool - { - return $this->isPropagationStopped; - } -} +cart = $cart; + $this->orderItem = $orderItem; + $this->settings = $settings; + } + + public function getCart(): Cart + { + return $this->cart; + } + + public function getOrderItem(): OrderItem + { + return $this->orderItem; + } + + public function getSettings(): array + { + return $this->settings; + } + + public function setPropagationStopped(bool $isPropagationStopped): void + { + $this->isPropagationStopped = $isPropagationStopped; + } + + public function isPropagationStopped(): bool + { + return $this->isPropagationStopped; + } +} diff --git a/Classes/Event/Order/SuccessEvent.php b/Classes/Event/Order/SuccessEvent.php index c989681..01b8351 100644 --- a/Classes/Event/Order/SuccessEvent.php +++ b/Classes/Event/Order/SuccessEvent.php @@ -1,70 +1,70 @@ -cart = $cart; - $this->orderItem = $orderItem; - $this->settings = $settings; - } - - public function getCart(): Cart - { - return $this->cart; - } - - public function getOrderItem(): OrderItem - { - return $this->orderItem; - } - - public function getSettings(): array - { - return $this->settings; - } - - public function setPropagationStopped(bool $isPropagationStopped): void - { - $this->isPropagationStopped = $isPropagationStopped; - } - - public function isPropagationStopped(): bool - { - return $this->isPropagationStopped; - } -} +cart = $cart; + $this->orderItem = $orderItem; + $this->settings = $settings; + } + + public function getCart(): Cart + { + return $this->cart; + } + + public function getOrderItem(): OrderItem + { + return $this->orderItem; + } + + public function getSettings(): array + { + return $this->settings; + } + + public function setPropagationStopped(bool $isPropagationStopped): void + { + $this->isPropagationStopped = $isPropagationStopped; + } + + public function isPropagationStopped(): bool + { + return $this->isPropagationStopped; + } +} diff --git a/Classes/EventListener/Order/Payment/ClearCart.php b/Classes/EventListener/Order/Payment/ClearCart.php index f10439f..3e0d1ef 100644 --- a/Classes/EventListener/Order/Payment/ClearCart.php +++ b/Classes/EventListener/Order/Payment/ClearCart.php @@ -1,37 +1,29 @@ -getOrderItem(); - - $provider = $orderItem->getPayment()->getProvider(); - - if (strpos($provider, 'PAYPAL') === 0) { - parent::__invoke($event); - } - } -} +getOrderItem(); + $provider = $orderItem->getPayment()->getProvider(); + + if (strpos($provider, 'PAYPAL') === 0) { + parent::__invoke($event); + } + } +} \ No newline at end of file diff --git a/Classes/EventListener/Order/Payment/ProviderRedirect.php b/Classes/EventListener/Order/Payment/ProviderRedirect.php index 78c7966..c71427c 100644 --- a/Classes/EventListener/Order/Payment/ProviderRedirect.php +++ b/Classes/EventListener/Order/Payment/ProviderRedirect.php @@ -1,294 +1,244 @@ -configurationManager = $configurationManager; - $this->persistenceManager = $persistenceManager; - $this->typoScriptService = $typoScriptService; - $this->uriBuilder = $uriBuilder; - $this->cartRepository = $cartRepository; - - $this->cartConf = $this->configurationManager->getConfiguration( - ConfigurationManager::CONFIGURATION_TYPE_FRAMEWORK, - 'Cart' - ); - - $this->cartPaypalConf = $this->configurationManager->getConfiguration( - ConfigurationManager::CONFIGURATION_TYPE_FRAMEWORK, - 'CartPaypal' - ); - } - - public function __invoke(PaymentEvent $event): void - { - $this->orderItem = $event->getOrderItem(); - - if ($this->orderItem->getPayment()->getProvider() !== 'PAYPAL') { - return; - } - - $this->cart = $event->getCart(); - - $cart = $this->saveCurrentCartToDatabase(); - - $this->cartSHash = $cart->getSHash(); - $this->cartFHash = $cart->getFHash(); - - $this->getQuery(); - - $paymentQueryString = http_build_query($this->paymentQuery); - header('Location: ' . $this->getQueryUrl() . $paymentQueryString); - - $event->setPropagationStopped(true); - } - - protected function saveCurrentCartToDatabase(): Cart - { - $cart = GeneralUtility::makeInstance(Cart::class); - - $cart->setOrderItem($this->orderItem); - $cart->setCart($this->cart); - $cart->setPid((int)$this->cartConf['settings']['order']['pid']); - - $this->cartRepository->add($cart); - $this->persistenceManager->persistAll(); - - return $cart; - } - - protected function getQueryUrl(): string - { - if ($this->cartPaypalConf['sandbox']) { - return self::PAYPAL_API_SANDBOX; - } - - return self::PAYPAL_API_LIVE; - } - - protected function getQuery(): void - { - $this->getQueryFromSettings(); - $this->getQueryFromCart(); - $this->getQueryFromOrder(); - } - - protected function getQueryFromSettings(): void - { - $this->paymentQuery['business'] = $this->cartPaypalConf['business']; - $this->paymentQuery['test_ipn'] = intval($this->cartPaypalConf['sandbox']); - - $this->paymentQuery['custom'] = $this->cartSHash; - $this->paymentQuery['notify_url'] = $this->getUrl('notify', $this->cartSHash); - $this->paymentQuery['return'] = $this->getUrl('success', $this->cartSHash); - $this->paymentQuery['cancel_return'] = $this->getUrl('cancel', $this->cartFHash); - - $this->paymentQuery['cmd'] = '_cart'; - $this->paymentQuery['upload'] = '1'; - - $this->paymentQuery['currency_code'] = $this->orderItem->getCurrencyCode(); - } - - protected function getQueryFromCart(): void - { - $this->paymentQuery['invoice'] = $this->cart->getOrderNumber(); - - if ($this->cartPaypalConf['sendEachItemToPaypal']) { - $this->addEachItemsFromCartToQuery(); - } else { - $this->addEntireCartToQuery(); - } - } - - protected function getQueryFromOrder(): void - { - $billingAddress = $this->orderItem->getBillingAddress(); - - $this->paymentQuery['first_name'] = $billingAddress->getFirstName(); - $this->paymentQuery['last_name'] = $billingAddress->getLastName(); - $this->paymentQuery['email'] = $billingAddress->getEmail(); - } - - protected function addEachItemsFromCartToQuery(): void - { - $shippingGross = $this->cart->getShipping()->getGross(); - $this->paymentQuery['handling_cart'] = number_format( - $shippingGross, - 2, - '.', - '' - ); - - $this->addEachCouponFromCartToQuery(); - $this->addEachProductFromCartToQuery(); - - $this->paymentQuery['mc_gross'] = number_format( - $this->cart->getTotalGross(), - 2, - '.', - '' - ); - } - - protected function addEachCouponFromCartToQuery(): void - { - if ($this->cart->getCoupons()) { - $discount = 0; - /** - * @var CartCoupon $cartCoupon - */ - foreach ($this->cart->getCoupons() as $cartCoupon) { - if ($cartCoupon->getIsUseable()) { - $discount += $cartCoupon->getDiscount(); - } - } - - $this->paymentQuery['discount_amount_cart'] = $discount; - } - } - - protected function addEachProductFromCartToQuery(): void - { - if ($this->orderItem->getProducts()) { - $count = 0; - foreach ($this->orderItem->getProducts() as $productKey => $product) { - $count += 1; - - $this->paymentQuery['item_name_' . $count] = $product->getTitle(); - $this->paymentQuery['quantity_' . $count] = $product->getCount(); - $this->paymentQuery['amount_' . $count] = number_format( - $product->getGross() / $product->getCount(), - 2, - '.', - '' - ); - } - } - } - - protected function addEntireCartToQuery(): void - { - $this->paymentQuery['quantity'] = 1; - $this->paymentQuery['mc_gross'] = number_format( - $this->cart->getGross() + $this->cart->getServiceGross(), - 2, - '.', - '' - ); - - $this->paymentQuery['item_name_1'] = $this->cartPaypalConf['sendEachItemToPaypalTitle']; - $this->paymentQuery['quantity_1'] = 1; - $this->paymentQuery['amount_1'] = $this->paymentQuery['mc_gross']; - } - - protected function getUrl(string $action, string $hash): string - { - $pid = (int)$this->cartConf['settings']['cart']['pid']; - - $arguments = [ - 'tx_cartpaypal_cart' => [ - 'controller' => 'Order\Payment', - 'order' => $this->orderItem->getUid(), - 'action' => $action, - 'hash' => $hash - ] - ]; - - return $this->uriBuilder->reset() - ->setTargetPageUid($pid) - ->setTargetPageType((int)$this->cartPaypalConf['redirectTypeNum']) - ->setCreateAbsoluteUri(true) - ->setArguments($arguments) - ->build(); - } -} +configurationManager = $configurationManager; + $this->persistenceManager = $persistenceManager; + $this->typoScriptService = $typoScriptService; + $this->cartRepository = $cartRepository; + + $this->cartConf = $this->configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_FRAMEWORK, + 'Cart' + ); + + $this->cartPaypalConf = $this->configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_FRAMEWORK, + 'CartPaypal' + ); + } + + public function __invoke(PaymentEvent $event): void + { + $this->orderItem = $event->getOrderItem(); + + if ($this->orderItem->getPayment()->getProvider() !== 'PAYPAL') { + return; + } + + $this->cart = $event->getCart(); + $cart = $this->saveCurrentCartToDatabase(); + + $this->cartSHash = $cart->getSHash(); + $this->cartFHash = $cart->getFHash(); + + $this->getQuery(); + + $paymentQueryString = http_build_query($this->paymentQuery); + + #echo $this->getQueryUrl() .' '. $paymentQueryString; + #exit; + header('Location: ' . $this->getQueryUrl() . $paymentQueryString); + exit; + } + + protected function saveCurrentCartToDatabase(): Cart + { + $cart = GeneralUtility::makeInstance(Cart::class); + + $cart->setOrderItem($this->orderItem); + $cart->setCart($this->cart); + $cart->setPid((int)$this->cartConf['settings']['order']['pid']); + + $this->cartRepository->add($cart); + $this->persistenceManager->persistAll(); + + return $cart; + } + + protected function getQueryUrl(): string + { + if ($this->cartPaypalConf['sandbox']) { + return self::PAYPAL_API_SANDBOX; + } + + return self::PAYPAL_API_LIVE; + } + + protected function getQuery(): void + { + $this->getQueryFromSettings(); + $this->getQueryFromCart(); + $this->getQueryFromOrder(); + } + + protected function getQueryFromSettings(): void + { + $this->paymentQuery['business'] = $this->cartPaypalConf['business']; + $this->paymentQuery['test_ipn'] = intval($this->cartPaypalConf['sandbox']); + + $this->paymentQuery['custom'] = $this->cartSHash; + $this->paymentQuery['notify_url'] = $this->getUrl('notify', $this->cartSHash); + $this->paymentQuery['return'] = $this->getUrl('success', $this->cartSHash); + $this->paymentQuery['cancel_return'] = $this->getUrl('cancel', $this->cartFHash); + + $this->paymentQuery['cmd'] = '_cart'; + $this->paymentQuery['upload'] = '1'; + + $this->paymentQuery['currency_code'] = $this->orderItem->getCurrencyCode(); + } + + protected function getQueryFromCart(): void + { + $this->paymentQuery['invoice'] = $this->cart->getOrderNumber(); + + if ($this->cartPaypalConf['sendEachItemToPaypal']) { + $this->addEachItemsFromCartToQuery(); + } else { + $this->addEntireCartToQuery(); + } + } + + protected function getQueryFromOrder(): void + { + $billingAddress = $this->orderItem->getBillingAddress(); + + $this->paymentQuery['first_name'] = $billingAddress->getFirstName(); + $this->paymentQuery['last_name'] = $billingAddress->getLastName(); + $this->paymentQuery['email'] = $billingAddress->getEmail(); + } + + protected function addEachItemsFromCartToQuery(): void + { + $shippingGross = $this->cart->getShipping()->getGross(); + $this->paymentQuery['handling_cart'] = number_format( + $shippingGross, + 2, + '.', + '' + ); + + $this->addEachCouponFromCartToQuery(); + $this->addEachProductFromCartToQuery(); + + $this->paymentQuery['mc_gross'] = number_format( + $this->cart->getTotalGross(), + 2, + '.', + '' + ); + } + + protected function addEachCouponFromCartToQuery(): void + { + if ($this->cart->getCoupons()) { + $discount = 0; + /** + * @var CartCoupon $cartCoupon + */ + foreach ($this->cart->getCoupons() as $cartCoupon) { + if ($cartCoupon->getIsUseable()) { + $discount += $cartCoupon->getDiscount(); + } + } + + $this->paymentQuery['discount_amount_cart'] = $discount; + } + } + + protected function addEachProductFromCartToQuery(): void + { + if ($this->orderItem->getProducts()) { + $count = 0; + foreach ($this->orderItem->getProducts() as $productKey => $product) { + $count += 1; + + $this->paymentQuery['item_name_' . $count] = $product->getTitle(); + $this->paymentQuery['quantity_' . $count] = $product->getCount(); + $this->paymentQuery['amount_' . $count] = number_format( + $product->getGross() / $product->getCount(), + 2, + '.', + '' + ); + } + } + } + + protected function addEntireCartToQuery(): void + { + $this->paymentQuery['quantity'] = 1; + $this->paymentQuery['mc_gross'] = number_format( + $this->cart->getGross() + $this->cart->getServiceGross(), + 2, + '.', + '' + ); + + $this->paymentQuery['item_name_1'] = $this->cartPaypalConf['sendEachItemToPaypalTitle']; + $this->paymentQuery['quantity_1'] = 1; + $this->paymentQuery['amount_1'] = $this->paymentQuery['mc_gross']; + } + + protected function getUrl(string $action, string $hash): string + { + $pid = (int)$this->cartConf['settings']['cart']['pid']; + $arguments = [ + 'tx_cartpaypal_cart' => [ + 'controller' => 'Order\Payment', + 'order' => $this->orderItem->getUid(), + 'action' => $action, + 'hash' => $hash + ] + ]; + + $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + $site = $siteFinder->getSiteByPageId($pid); + + return (string)$site->getRouter()->generateUri( + $pid, + [ + 'type' => (int)$this->cartPaypalConf['redirectTypeNum'], + '_language' => $GLOBALS['TYPO3_REQUEST']->getAttribute('language')->getLanguageId(), + ] + $arguments + ); + } +} \ No newline at end of file diff --git a/Classes/Updates/ExtcodeCartPaypalCTypeMigration.php b/Classes/Updates/ExtcodeCartPaypalCTypeMigration.php new file mode 100644 index 0000000..9ac46f7 --- /dev/null +++ b/Classes/Updates/ExtcodeCartPaypalCTypeMigration.php @@ -0,0 +1,35 @@ + + */ + protected function getListTypeToCTypeMapping(): array + { + return [ + // list_type => CType + 'cartpaypal_payment' => 'cartpaypal_payment', + ]; + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index dc17ee4..450499e 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -5,8 +5,8 @@ services: public: false Extcode\CartPaypal\: - resource: '../Classes/*' - exclude: '../Classes/Widgets/*' + resource: "../Classes/*" + exclude: "../Classes/Widgets/*" Extcode\CartPaypal\Controller\Order\PaymentController: arguments: @@ -19,13 +19,13 @@ services: Extcode\CartPaypal\EventListener\Order\Payment\ClearCart: arguments: $cartUtility: '@Extcode\Cart\Utility\CartUtility' - $parserUtility: '@Extcode\Cart\Utility\ParserUtility' + $paymentMethodsService: '@Extcode\Cart\Service\PaymentMethodsServiceInterface' $sessionHandler: '@Extcode\Cart\Service\SessionHandler' tags: - name: event.listener - identifier: 'cart-paypal--order--payment--clear-cart' + identifier: "cart-paypal--order--payment--clear-cart" event: Extcode\Cart\Event\Order\PaymentEvent - before: 'cart-paypal--order--payment--provider-redirect' + before: "cart-paypal--order--payment--provider-redirect" Extcode\CartPaypal\EventListener\Order\Payment\ProviderRedirect: arguments: @@ -35,12 +35,12 @@ services: $cartRepository: '@Extcode\Cart\Domain\Repository\CartRepository' tags: - name: event.listener - identifier: 'cart-paypal--order--payment--provider-redirect' + identifier: "cart-paypal--order--payment--provider-redirect" event: Extcode\Cart\Event\Order\PaymentEvent Extcode\CartPaypal\EventListener\Order\Notify\Email: class: 'Extcode\Cart\EventListener\Order\Finish\Email' tags: - name: event.listener - identifier: 'cart-paypal--order--notify--email' + identifier: "cart-paypal--order--notify--email" event: Extcode\CartPaypal\Event\Order\NotifyEvent diff --git a/Configuration/TCA/Overrides/sys_template.php b/Configuration/TCA/Overrides/sys_template.php index 6f17b50..f088c68 100644 --- a/Configuration/TCA/Overrides/sys_template.php +++ b/Configuration/TCA/Overrides/sys_template.php @@ -1,12 +1,12 @@ - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Resources/Private/Layouts/Default.html b/Resources/Private/Layouts/Default.html index 1bbd2ea..39275e0 100644 --- a/Resources/Private/Layouts/Default.html +++ b/Resources/Private/Layouts/Default.html @@ -1,3 +1,3 @@ -
- +
+
\ No newline at end of file diff --git a/Resources/Private/Templates/Order/Payment/Cancel.html b/Resources/Private/Templates/Order/Payment/Cancel.html index fcb9b98..1d12760 100644 --- a/Resources/Private/Templates/Order/Payment/Cancel.html +++ b/Resources/Private/Templates/Order/Payment/Cancel.html @@ -1,11 +1,11 @@ - - - - - - -
- -
-
-
+ + + + + + +
+ +
+
+
diff --git a/Resources/Private/Templates/Order/Payment/Confirm.html b/Resources/Private/Templates/Order/Payment/Confirm.html index fedd8a4..8214108 100644 --- a/Resources/Private/Templates/Order/Payment/Confirm.html +++ b/Resources/Private/Templates/Order/Payment/Confirm.html @@ -1,14 +1,14 @@ - - - - - - -
- -
-
- -
-
-
+ + + + + + +
+ +
+
+ +
+
+
diff --git a/Resources/Private/Templates/Order/Payment/Notify.html b/Resources/Private/Templates/Order/Payment/Notify.html index fcb9b98..1d12760 100644 --- a/Resources/Private/Templates/Order/Payment/Notify.html +++ b/Resources/Private/Templates/Order/Payment/Notify.html @@ -1,11 +1,11 @@ - - - - - - -
- -
-
-
+ + + + + + +
+ +
+
+
diff --git a/Resources/Private/Templates/Order/Payment/Success.html b/Resources/Private/Templates/Order/Payment/Success.html index fedd8a4..8214108 100644 --- a/Resources/Private/Templates/Order/Payment/Success.html +++ b/Resources/Private/Templates/Order/Payment/Success.html @@ -1,14 +1,14 @@ - - - - - - -
- -
-
- -
-
-
+ + + + + + +
+ +
+
+ +
+
+
diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg index 4f1fead..2edf85f 100644 --- a/Resources/Public/Icons/Extension.svg +++ b/Resources/Public/Icons/Extension.svg @@ -1,16 +1,16 @@ - - - - - - - - - - - - PayPal - - - + + + + + + + + + + + + PayPal + + + diff --git a/composer.json b/composer.json index 58c2b88..4e7b1bc 100644 --- a/composer.json +++ b/composer.json @@ -1,92 +1,91 @@ { - "name": "extcode/cart-paypal", - "type": "typo3-cms-extension", - "description": "Shopping Cart(s) for TYPO3 - PayPal Payment Provider", - "homepage": "https://cart.extco.de", - "license": [ - "GPL-2.0+" - ], - "keywords": [ - "TYPO3 CMS", - "Shopping Cart", - "PayPal", - "cart" - ], - "authors": [ - { - "name": "Daniel Gohlke", - "email": "ext.cart@extco.de", - "role": "Developer" - } - ], - "support": { - "issues": "https://github.com/extcode/cart_paypal/issues" - }, - "autoload": { - "psr-4": { - "Extcode\\CartPaypal\\": "Classes" - } - }, - "config": { - "bin-dir": ".build/bin", - "vendor-dir": ".build/vendor" - }, - "extra": { - "typo3/cms": { - "extension-key": "cart_paypal", - "app-dir": ".build", - "web-dir": ".build/public" - } - }, - "require": { - "php": "^7.2 || ^8.0", - "ext-curl": "*", - "typo3/cms-core": "^10.4 || ^11.5", - "typo3/cms-extbase": "^10.4 || ^11.5", - "typo3/cms-frontend": "^10.4 || ^11.5", - "extcode/cart": "^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.14", - "helmich/typo3-typoscript-lint": "^2.0", - "overtrue/phplint": "^1.1", - "rector/rector": "^0.11", - "phpstan/phpstan": "^0.12" - - }, - "scripts": { - "test:cgl": [ - ".build/bin/php-cs-fixer fix --config=Build/.php-cs-fixer.dist.php -v --using-cache=no --path-mode=intersection ./" - ], - "test:cgl:dry-run": [ - ".build/bin/php-cs-fixer fix --config=Build/.php-cs-fixer.dist.php -v --dry-run --using-cache=no --path-mode=intersection ./" - ], - "test:php:lint": [ - ".build/bin/phplint -c Build/phplint.yaml" - ], - "test:phpstan:analyse": [ - ".build/bin/phpstan analyse -c Build/phpstan.neon" - ], - "test:rector:process": [ - ".build/bin/rector process *" - ], - "test:rector:process:dry-run": [ - ".build/bin/rector process * --dry-run" - ], - "test:typoscript:lint": [ - ".build/bin/typoscript-lint -c Build/typoscriptlint.yaml Configuration" - ], - "test:php": [ - "@test:php:lint" - ], - "test:all": [ - "@test:cgl", - "@test:php", - "@test:typoscript:lint" - ], - "post-autoload-dump": [ - "mkdir -p .build/Web/typo3conf/ext/", - "[ -L .build/Web/typo3conf/ext/cart_paypal ] || ln -snvf ../../../../. .build/Web/typo3conf/ext/cart_paypal" - ] - } + "name": "extcode/cart-paypal", + "type": "typo3-cms-extension", + "description": "Shopping Cart(s) for TYPO3 - PayPal Payment Provider", + "homepage": "https://cart.extco.de", + "license": [ + "GPL-2.0+" + ], + "keywords": [ + "TYPO3 CMS", + "Shopping Cart", + "PayPal", + "cart" + ], + "authors": [ + { + "name": "Daniel Gohlke", + "email": "ext.cart@extco.de", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/extcode/cart_paypal/issues" + }, + "autoload": { + "psr-4": { + "Extcode\\CartPaypal\\": "Classes" + } + }, + "config": { + "bin-dir": ".build/bin", + "vendor-dir": ".build/vendor" + }, + "extra": { + "typo3/cms": { + "extension-key": "cart_paypal", + "app-dir": ".build", + "web-dir": ".build/public" + } + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "ext-curl": "*", + "typo3/cms-core": "^13.4", + "typo3/cms-extbase": "^13.4", + "typo3/cms-frontend": "^13.4", + "extcode/cart": "^11.5" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64", + "helmich/typo3-typoscript-lint": "^3.3", + "overtrue/phplint": "^9.6", + "rector/rector": "^2.9", + "phpstan/phpstan": "^1.12" + }, + "scripts": { + "test:cgl": [ + ".build/bin/php-cs-fixer fix --config=Build/.php-cs-fixer.dist.php -v --using-cache=no --path-mode=intersection ./" + ], + "test:cgl:dry-run": [ + ".build/bin/php-cs-fixer fix --config=Build/.php-cs-fixer.dist.php -v --dry-run --using-cache=no --path-mode=intersection ./" + ], + "test:php:lint": [ + ".build/bin/phplint -c Build/phplint.yaml" + ], + "test:phpstan:analyse": [ + ".build/bin/phpstan analyse -c Build/phpstan.neon" + ], + "test:rector:process": [ + ".build/bin/rector process *" + ], + "test:rector:process:dry-run": [ + ".build/bin/rector process * --dry-run" + ], + "test:typoscript:lint": [ + ".build/bin/typoscript-lint -c Build/typoscriptlint.yaml Configuration" + ], + "test:php": [ + "@test:php:lint" + ], + "test:all": [ + "@test:cgl", + "@test:php", + "@test:typoscript:lint" + ], + "post-autoload-dump": [ + "mkdir -p .build/Web/typo3conf/ext/", + "[ -L .build/Web/typo3conf/ext/cart_paypal ] || ln -snvf ../../../../. .build/Web/typo3conf/ext/cart_paypal" + ] + } } diff --git a/ext_emconf.php b/ext_emconf.php index 9145b77..cef1d0d 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,29 +1,20 @@ - 'Cart - PayPal', - 'description' => 'Shopping Cart(s) for TYPO3 - PayPal Payment Provider', - 'category' => 'services', - 'author' => 'Daniel Gohlke', - 'author_email' => 'ext.cart@extco.de', - 'author_company' => 'extco.de UG (haftungsbeschränkt)', - 'shy' => '', - 'priority' => '', - 'module' => '', - 'state' => 'stable', - 'internal' => '', - 'uploadfolder' => '0', - 'createDirs' => '', - 'modify_tables' => '', - 'clearCacheOnLoad' => 0, - 'lockType' => '', - 'version' => '6.0.0', - 'constraints' => [ - 'depends' => [ - 'typo3' => '10.4.0-11.5.99', - 'cart' => '8.0.0', - ], - 'conflicts' => [], - 'suggests' => [], - ], -]; + 'Cart - PayPal', + 'description' => 'Shopping Cart(s) for TYPO3 - PayPal Payment Provider', + 'category' => 'services', + 'author' => 'Daniel Gohlke', + 'author_email' => 'ext.cart@extco.de', + 'author_company' => 'extco.de UG (haftungsbeschränkt)', + 'state' => 'stable', + 'version' => '99.0.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '13.4.0-13.4.99', + 'cart' => '11.5.0', + ], + 'conflicts' => [], + 'suggests' => [], + ], +]; diff --git a/ext_localconf.php b/ext_localconf.php index a582ece..38f857f 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,16 +1,17 @@ - 'success, cancel, notify', - ], - [ - \Extcode\CartPaypal\Controller\Order\PaymentController::class => 'success, cancel, notify', - ] -); + 'success, cancel, notify', + ], + [ + \Extcode\CartPaypal\Controller\Order\PaymentController::class => 'success, cancel, notify', + ], + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT +); diff --git a/rector.php b/rector.php index 6d23eb8..3b4a12b 100644 --- a/rector.php +++ b/rector.php @@ -1,115 +1,115 @@ -parameters(); - - $containerConfigurator->import(Typo3SetList::TYPO3_76); - $containerConfigurator->import(Typo3SetList::TYPO3_87); - $containerConfigurator->import(Typo3SetList::TYPO3_95); - $containerConfigurator->import(Typo3SetList::TYPO3_104); - - // In order to have a better analysis from phpstan we teach it here some more things - $parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, Typo3Option::PHPSTAN_FOR_RECTOR_PATH); - - // FQN classes are not imported by default. If you don't do it manually after every Rector run, enable it by: - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - - // this will not import root namespace classes, like \DateTime or \Exception - $parameters->set(Option::IMPORT_SHORT_CLASSES, false); - - // this will not import classes used in PHP DocBlocks, like in /** @var \Some\Class */ - $parameters->set(Option::IMPORT_DOC_BLOCKS, false); - - // Define your target version which you want to support - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_72); - - // If you have an editorconfig and changed files should keep their format enable it here - // $parameters->set(Option::ENABLE_EDITORCONFIG, true); - - // If you only want to process one/some TYPO3 extension(s), you can specify its path(s) here. - // If you use the option --config change __DIR__ to getcwd() - // $parameters->set(Option::PATHS, [ - // __DIR__ . '/packages/acme_demo/', - // ]); - - // If you set option Option::AUTO_IMPORT_NAMES to true, you should consider excluding some TYPO3 files. - // If you use the option --config change __DIR__ to getcwd() - $parameters->set(Option::SKIP, [ - NameImportingPostRector::class => [ - 'ClassAliasMap.php', - 'ext_emconf.php', - 'ext_localconf.php', - 'ext_tables.php', - __DIR__ . '/**/Configuration/AjaxRoutes.php', - __DIR__ . '/**/Configuration/Backend/AjaxRoutes.php', - __DIR__ . '/**/Configuration/Commands.php', - __DIR__ . '/**/Configuration/ExpressionLanguage.php', - __DIR__ . '/**/Configuration/Extbase/Persistence/Classes.php', - __DIR__ . '/**/Configuration/RequestMiddlewares.php', - __DIR__ . '/**/Configuration/TCA/*', - ], - // @see https://github.com/sabbelasichon/typo3-rector/issues/2536 - __DIR__ . '/**/Configuration/ExtensionBuilder/*', - // We skip those directories on purpose as there might be node_modules or similar - // that include typescript which would result in false positive processing - __DIR__ . '/**/Resources/**/node_modules/*', - __DIR__ . '/**/Resources/**/NodeModules/*', - __DIR__ . '/**/Resources/**/BowerComponents/*', - __DIR__ . '/**/Resources/**/bower_components/*', - __DIR__ . '/**/Resources/**/build/*', - ]); - - // If you have trouble that rector cannot run because some TYPO3 constants are not defined add an additional constants file - // @see https://github.com/sabbelasichon/typo3-rector/blob/master/typo3.constants.php - // @see https://github.com/rectorphp/rector/blob/main/docs/static_reflection_and_autoload.md#include-files - // $parameters->set(Option::BOOTSTRAP_FILES, [ - // __DIR__ . '/typo3.constants.php' - // ]); - - // get services (needed for register a single rule) - $services = $containerConfigurator->services(); - - // register a single rule - // $services->set(InjectAnnotationRector::class); - - /** - * Useful rule from RectorPHP itself to transform i.e. GeneralUtility::makeInstance('TYPO3\CMS\Core\Log\LogManager') - * to GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class) calls. - * But be warned, sometimes it produces false positives (edge cases), so watch out - */ - // $services->set(StringClassNameToClassConstantRector::class); - - // Optional non-php file functionalities: - // @see https://github.com/sabbelasichon/typo3-rector/blob/main/docs/beyond_php_file_processors.md - - // Adapt your composer.json dependencies to the latest available version for the defined SetList - // $containerConfigurator->import(Typo3SetList::COMPOSER_PACKAGES_104_CORE); - // $containerConfigurator->import(Typo3SetList::COMPOSER_PACKAGES_104_EXTENSIONS); - - // Rewrite your extbase persistence class mapping from typoscript into php according to official docs. - // This processor will create a summarized file with all of the typoscript rewrites combined into a single file. - // The filename can be passed as argument, "Configuration_Extbase_Persistence_Classes.php" is default. - // $services->set(ExtbasePersistenceTypoScriptRector::class); - // Add some general TYPO3 rules - $services->set(ConvertTypo3ConfVarsRector::class); - $services->set(ExtEmConfRector::class); - $services->set(ExtensionComposerRector::class); - - // Do you want to modernize your TypoScript include statements for files and move from to @import use the FileIncludeToImportStatementVisitor - // $services->set(FileIncludeToImportStatementTypoScriptRector::class); -}; +parameters(); + + $containerConfigurator->import(Typo3SetList::TYPO3_76); + $containerConfigurator->import(Typo3SetList::TYPO3_87); + $containerConfigurator->import(Typo3SetList::TYPO3_95); + $containerConfigurator->import(Typo3SetList::TYPO3_104); + + // In order to have a better analysis from phpstan we teach it here some more things + $parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, Typo3Option::PHPSTAN_FOR_RECTOR_PATH); + + // FQN classes are not imported by default. If you don't do it manually after every Rector run, enable it by: + $parameters->set(Option::AUTO_IMPORT_NAMES, true); + + // this will not import root namespace classes, like \DateTime or \Exception + $parameters->set(Option::IMPORT_SHORT_CLASSES, false); + + // this will not import classes used in PHP DocBlocks, like in /** @var \Some\Class */ + $parameters->set(Option::IMPORT_DOC_BLOCKS, false); + + // Define your target version which you want to support + $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_72); + + // If you have an editorconfig and changed files should keep their format enable it here + // $parameters->set(Option::ENABLE_EDITORCONFIG, true); + + // If you only want to process one/some TYPO3 extension(s), you can specify its path(s) here. + // If you use the option --config change __DIR__ to getcwd() + // $parameters->set(Option::PATHS, [ + // __DIR__ . '/packages/acme_demo/', + // ]); + + // If you set option Option::AUTO_IMPORT_NAMES to true, you should consider excluding some TYPO3 files. + // If you use the option --config change __DIR__ to getcwd() + $parameters->set(Option::SKIP, [ + NameImportingPostRector::class => [ + 'ClassAliasMap.php', + 'ext_emconf.php', + 'ext_localconf.php', + 'ext_tables.php', + __DIR__ . '/**/Configuration/AjaxRoutes.php', + __DIR__ . '/**/Configuration/Backend/AjaxRoutes.php', + __DIR__ . '/**/Configuration/Commands.php', + __DIR__ . '/**/Configuration/ExpressionLanguage.php', + __DIR__ . '/**/Configuration/Extbase/Persistence/Classes.php', + __DIR__ . '/**/Configuration/RequestMiddlewares.php', + __DIR__ . '/**/Configuration/TCA/*', + ], + // @see https://github.com/sabbelasichon/typo3-rector/issues/2536 + __DIR__ . '/**/Configuration/ExtensionBuilder/*', + // We skip those directories on purpose as there might be node_modules or similar + // that include typescript which would result in false positive processing + __DIR__ . '/**/Resources/**/node_modules/*', + __DIR__ . '/**/Resources/**/NodeModules/*', + __DIR__ . '/**/Resources/**/BowerComponents/*', + __DIR__ . '/**/Resources/**/bower_components/*', + __DIR__ . '/**/Resources/**/build/*', + ]); + + // If you have trouble that rector cannot run because some TYPO3 constants are not defined add an additional constants file + // @see https://github.com/sabbelasichon/typo3-rector/blob/master/typo3.constants.php + // @see https://github.com/rectorphp/rector/blob/main/docs/static_reflection_and_autoload.md#include-files + // $parameters->set(Option::BOOTSTRAP_FILES, [ + // __DIR__ . '/typo3.constants.php' + // ]); + + // get services (needed for register a single rule) + $services = $containerConfigurator->services(); + + // register a single rule + // $services->set(InjectAnnotationRector::class); + + /** + * Useful rule from RectorPHP itself to transform i.e. GeneralUtility::makeInstance('TYPO3\CMS\Core\Log\LogManager') + * to GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class) calls. + * But be warned, sometimes it produces false positives (edge cases), so watch out + */ + // $services->set(StringClassNameToClassConstantRector::class); + + // Optional non-php file functionalities: + // @see https://github.com/sabbelasichon/typo3-rector/blob/main/docs/beyond_php_file_processors.md + + // Adapt your composer.json dependencies to the latest available version for the defined SetList + // $containerConfigurator->import(Typo3SetList::COMPOSER_PACKAGES_104_CORE); + // $containerConfigurator->import(Typo3SetList::COMPOSER_PACKAGES_104_EXTENSIONS); + + // Rewrite your extbase persistence class mapping from typoscript into php according to official docs. + // This processor will create a summarized file with all of the typoscript rewrites combined into a single file. + // The filename can be passed as argument, "Configuration_Extbase_Persistence_Classes.php" is default. + // $services->set(ExtbasePersistenceTypoScriptRector::class); + // Add some general TYPO3 rules + $services->set(ConvertTypo3ConfVarsRector::class); + $services->set(ExtEmConfRector::class); + $services->set(ExtensionComposerRector::class); + + // Do you want to modernize your TypoScript include statements for files and move from to @import use the FileIncludeToImportStatementVisitor + // $services->set(FileIncludeToImportStatementTypoScriptRector::class); +}; From 0deed963cc8d0f7490f59233da6f198b3982ff9a Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 12 Nov 2025 11:04:04 +0100 Subject: [PATCH 2/2] include new PayPal APIv2 --- .../Controller/Order/PaymentController.php | 545 +++++++++++------- .../Order/Payment/ProviderRedirect.php | 320 ++++++---- Configuration/TypoScript/setup.typoscript | 22 +- 3 files changed, 577 insertions(+), 310 deletions(-) diff --git a/Classes/Controller/Order/PaymentController.php b/Classes/Controller/Order/PaymentController.php index 4bacb0d..010ff55 100644 --- a/Classes/Controller/Order/PaymentController.php +++ b/Classes/Controller/Order/PaymentController.php @@ -26,12 +26,10 @@ use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +use TYPO3\CMS\Core\Http\RequestFactory; class PaymentController extends ActionController { - const PAYPAL_API_SANDBOX = 'https://www.sandbox.paypal.com/cgi-bin/webscr?'; - const PAYPAL_API_LIVE = 'https://www.paypal.com/cgi-bin/webscr?'; - /** * @var LoggerInterface */ @@ -57,6 +55,11 @@ class PaymentController extends ActionController */ protected $paymentRepository; + /** + * @var RequestFactory + */ + protected $requestFactory; + /** * @var Cart */ @@ -72,18 +75,25 @@ class PaymentController extends ActionController */ protected $cartPaypalConf = []; + /** + * @var string|null + */ + protected $accessToken = null; + public function __construct( LogManagerInterface $logManager, PersistenceManager $persistenceManager, SessionHandler $sessionHandler, CartRepository $cartRepository, - PaymentRepository $paymentRepository + PaymentRepository $paymentRepository, + RequestFactory $requestFactory ) { $this->logger = $logManager->getLogger(); $this->persistenceManager = $persistenceManager; $this->sessionHandler = $sessionHandler; $this->cartRepository = $cartRepository; $this->paymentRepository = $paymentRepository; + $this->requestFactory = $requestFactory; } protected function initializeAction(): void @@ -103,27 +113,9 @@ protected function initializeAction(): void public function successAction(): ResponseInterface { - if ($this->request->hasArgument('hash') && !empty($this->request->getArgument('hash'))) { - $this->loadCartByHash($this->request->getArgument('hash')); - - if ($this->cart) { - $orderItem = $this->cart->getOrderItem(); - - $successEvent = new SuccessEvent($this->cart->getCart(), $orderItem, $this->cartConf); - $this->eventDispatcher->dispatch($successEvent); - - return $this->redirect('show', 'Cart\Order', 'Cart', ['orderItem' => $orderItem]); - } else { - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.success.error_occured', - 'CartPaypal' - ), - '', - \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR - ); - } - } else { + $token = $this->request->getQueryParams()['token'] ?? ''; + + if (!$token) { $this->addFlashMessage( LocalizationUtility::translate( 'tx_cartpaypal.controller.order.payment.action.success.access_denied', @@ -132,47 +124,49 @@ public function successAction(): ResponseInterface '', \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR ); + return $this->redirect('show', 'Cart\Cart', 'Cart'); + } + + $this->loadCartByPaypalOrderId($token); + + if (!$this->cart) { + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.success.error_occured', + 'CartPaypal' + ), + '', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + return $this->redirect('show', 'Cart\Cart', 'Cart'); + } + + // Capture the PayPal order + if ($this->capturePayPalOrder($token)) { + $orderItem = $this->cart->getOrderItem(); + + $successEvent = new SuccessEvent($this->cart->getCart(), $orderItem, $this->cartConf); + $this->eventDispatcher->dispatch($successEvent); + + return $this->redirect('show', 'Cart\Order', 'Cart', ['orderItem' => $orderItem]); + } else { + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.success.payment_failed', + 'CartPaypal' + ), + '', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + return $this->redirect('show', 'Cart\Cart', 'Cart'); } } public function cancelAction(): ResponseInterface { - if ($this->request->hasArgument('hash') && !empty($this->request->getArgument('hash'))) { - $this->loadCartByHash($this->request->getArgument('hash'), 'FHash'); - - if ($this->cart) { - $orderItem = $this->cart->getOrderItem(); - $payment = $orderItem->getPayment(); - - $this->restoreCartSession(); - - $payment->setStatus('canceled'); - - $this->paymentRepository->update($payment); - $this->persistenceManager->persistAll(); - - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.cancel.successfully_canceled', - 'CartPaypal' - ) - ); - - $cancelEvent = new CancelEvent($this->cart->getCart(), $orderItem, $this->cartConf); - $this->eventDispatcher->dispatch($cancelEvent); - - return $this->redirect('show', 'Cart\Cart', 'Cart'); - } else { - $this->addFlashMessage( - LocalizationUtility::translate( - 'tx_cartpaypal.controller.order.payment.action.cancel.error_occured', - 'CartPaypal' - ), - '', - \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR - ); - } - } else { + $token = $this->request->getQueryParams()['token'] ?? ''; + + if (!$token) { $this->addFlashMessage( LocalizationUtility::translate( 'tx_cartpaypal.controller.order.payment.action.cancel.access_denied', @@ -181,184 +175,351 @@ public function cancelAction(): ResponseInterface '', \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR ); + return $this->redirect('show', 'Cart\Cart', 'Cart'); + } + + $this->loadCartByPaypalOrderId($token); + + if ($this->cart) { + $orderItem = $this->cart->getOrderItem(); + $payment = $orderItem->getPayment(); + + $this->restoreCartSession(); + + $payment->setStatus('canceled'); + $this->paymentRepository->update($payment); + $this->persistenceManager->persistAll(); + + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.cancel.successfully_canceled', + 'CartPaypal' + ) + ); + + $cancelEvent = new CancelEvent($this->cart->getCart(), $orderItem, $this->cartConf); + $this->eventDispatcher->dispatch($cancelEvent); + } else { + $this->addFlashMessage( + LocalizationUtility::translate( + 'tx_cartpaypal.controller.order.payment.action.cancel.error_occured', + 'CartPaypal' + ), + '', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); } + + return $this->redirect('show', 'Cart\Cart', 'Cart'); } - public function notifyAction(): \Psr\Http\Message\ResponseInterface + public function notifyAction(): ResponseInterface { - - if ($this->request->getMethod() !== 'POST') { - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(405); - exit(); - } + if ($this->request->getMethod() !== 'POST') { return $this->htmlResponse()->withStatus(405, 'Method not allowed.'); } - $postData = $this->request->getParsedBody(); - $curlRequest = $this->getCurlRequestFromPostData($postData); - - if ( - is_array($this->cartPaypalConf) && - array_key_exists('debug', $this->cartPaypalConf) && - $this->cartPaypalConf['debug'] - ) { - $this->logger->debug( - 'Log Data', - [ - '$parsedPostData' => $postData, - '$curlRequest' => $curlRequest + $webhookBody = $this->request->getBody()->getContents(); + $webhookData = json_decode($webhookBody, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->error('Invalid JSON in PayPal webhook: ' . $webhookBody); + return $this->htmlResponse()->withStatus(400, 'Invalid JSON'); + } + + // Verify webhook signature + if (!$this->verifyWebhookSignature($webhookData)) { + $this->logger->warning('PayPal webhook signature verification failed'); + return $this->htmlResponse()->withStatus(401, 'Signature verification failed'); + } + + $eventType = $webhookData['event_type'] ?? ''; + $resource = $webhookData['resource'] ?? []; + + $this->logger->debug('PayPal Webhook received', [ + 'event_type' => $eventType, + 'resource' => $resource + ]); + + switch ($eventType) { + case 'CHECKOUT.ORDER.APPROVED': + case 'PAYMENT.CAPTURE.COMPLETED': + $this->handlePaymentCompleted($resource); + break; + + case 'PAYMENT.CAPTURE.DENIED': + case 'PAYMENT.CAPTURE.FAILED': + $this->handlePaymentFailed($resource); + break; + + case 'PAYMENT.CAPTURE.REFUNDED': + $this->handlePaymentRefunded($resource); + break; + + default: + $this->logger->info('Unhandled PayPal webhook event: ' . $eventType); + } + + return $this->htmlResponse()->withStatus(200); + } + + protected function capturePayPalOrder(string $orderId): bool + { + try { + $accessToken = $this->getAccessToken(); + if (!$accessToken) { + throw new \Exception('Could not get access token'); + } + + $captureUrl = $this->getApiBaseUrl() . '/v2/checkout/orders/' . $orderId . '/capture'; + + $response = $this->requestFactory->request($captureUrl, 'POST', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $accessToken, + 'Prefer' => 'return=representation' ] - ); - } - - $this->execCurlRequest($curlRequest); - $cartSHash = $postData['custom']; - if (empty($cartSHash)) { - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(403); - exit(); + ]); + + $responseData = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() === 201 || $response->getStatusCode() === 200) { + if ($responseData['status'] === 'COMPLETED') { + $orderItem = $this->cart->getOrderItem(); + $payment = $orderItem->getPayment(); + + $payment->setStatus('paid'); + + // Die PayPal Order ID wird bereits in der Cart-Entität gespeichert + // Wir müssen sie nicht zusätzlich in der Payment-Entität speichern + // Die Zuordnung erfolgt über die Cart-Entität + + $this->paymentRepository->update($payment); + $this->persistenceManager->persistAll(); + + $this->logger->info('PayPal order captured successfully', ['order_id' => $orderId]); + return true; + } } - return $this->htmlResponse()->withStatus(403, 'Not allowed.'); + + $this->logger->error('PayPal capture failed', [ + 'order_id' => $orderId, + 'status_code' => $response->getStatusCode(), + 'response' => $responseData + ]); + + } catch (\Exception $e) { + $this->logger->error('PayPal capture error: ' . $e->getMessage(), [ + 'order_id' => $orderId, + 'exception' => $e + ]); } - $this->loadCartByHash($this->request->getArgument('hash')); + return false; + } - if ($this->cart === null) { - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(404); - exit(); + protected function getAccessToken(): ?string + { + if ($this->accessToken !== null) { + return $this->accessToken; + } + + try { + $authUrl = $this->getApiBaseUrl() . '/v1/oauth2/token'; + $clientId = $this->cartPaypalConf['clientId'] ?? ''; + $clientSecret = $this->cartPaypalConf['clientSecret'] ?? ''; + + if (empty($clientId) || empty($clientSecret)) { + throw new \Exception('PayPal client ID or secret not configured'); + } + + $response = $this->requestFactory->request($authUrl, 'POST', [ + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Basic ' . base64_encode($clientId . ':' . $clientSecret), + 'Content-Type' => 'application/x-www-form-urlencoded' + ], + 'body' => 'grant_type=client_credentials' + ]); + + $data = json_decode($response->getBody()->getContents(), true); + + if (isset($data['access_token'])) { + $this->accessToken = $data['access_token']; + return $this->accessToken; + } + + } catch (\Exception $e) { + $this->logger->error('PayPal access token error: ' . $e->getMessage()); + } + + return null; + } + + protected function verifyWebhookSignature(array $webhookData): bool + { + $transmissionId = $this->request->getHeader('Paypal-Transmission-Id')[0] ?? ''; + $transmissionTime = $this->request->getHeader('Paypal-Transmission-Time')[0] ?? ''; + $transmissionSig = $this->request->getHeader('Paypal-Transmission-Sig')[0] ?? ''; + $certUrl = $this->request->getHeader('Paypal-Cert-Url')[0] ?? ''; + $authAlgo = $this->request->getHeader('Paypal-Auth-Algo')[0] ?? ''; + $webhookId = $this->cartPaypalConf['webhookId'] ?? ''; + + if (empty($transmissionId) || empty($transmissionTime) || empty($transmissionSig) || + empty($certUrl) || empty($authAlgo) || empty($webhookId)) { + return false; + } + + // In production, implement proper signature verification + // For now, we'll do a basic verification + $expectedWebhookId = $this->cartPaypalConf['webhookId'] ?? ''; + + if (!empty($expectedWebhookId) && isset($webhookData['id'])) { + // Simple verification - in production use proper cryptographic verification + return $this->verifyWebhookWithPayPal($webhookData, $transmissionId, $transmissionTime, $transmissionSig, $certUrl, $authAlgo, $webhookId); + } + + return true; // For development without webhook ID configured + } + + protected function verifyWebhookWithPayPal(array $webhookData, string $transmissionId, string $transmissionTime, string $transmissionSig, string $certUrl, string $authAlgo, string $webhookId): bool + { + try { + $accessToken = $this->getAccessToken(); + if (!$accessToken) { + return false; } - return $this->htmlResponse()->withStatus(404, 'Page / Cart not found.'); + + $verifyUrl = $this->getApiBaseUrl() . '/v1/notifications/verify-webhook-signature'; + + $verificationData = [ + 'transmission_id' => $transmissionId, + 'transmission_time' => $transmissionTime, + 'transmission_sig' => $transmissionSig, + 'cert_url' => $certUrl, + 'auth_algo' => $authAlgo, + 'webhook_id' => $webhookId, + 'webhook_event' => $webhookData + ]; + + $response = $this->requestFactory->request($verifyUrl, 'POST', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $accessToken + ], + 'body' => json_encode($verificationData) + ]); + + $result = json_decode($response->getBody()->getContents(), true); + + return isset($result['verification_status']) && $result['verification_status'] === 'SUCCESS'; + + } catch (\Exception $e) { + $this->logger->error('PayPal webhook verification error: ' . $e->getMessage()); + return false; + } + } + + protected function handlePaymentCompleted(array $resource): void + { + $orderId = $resource['id'] ?? $resource['order_id'] ?? ''; + + if (!$orderId) { + $this->logger->error('No order ID in completed payment webhook'); + return; + } + + $this->loadCartByPaypalOrderId($orderId); + + if ($this->cart === null) { + $this->logger->error('Cart not found for PayPal order: ' . $orderId); + return; } $orderItem = $this->cart->getOrderItem(); $payment = $orderItem->getPayment(); - if ($payment->getStatus() !== 'paid' && $postData['payment_status'] == 'Completed') { + if ($payment->getStatus() !== 'paid') { $payment->setStatus('paid'); + + // Die PayPal Order ID ist bereits in der Cart-Entität gespeichert + // Die Zuordnung erfolgt über die Cart-Entität + $this->paymentRepository->update($payment); $this->persistenceManager->persistAll(); $notifyEvent = new NotifyEvent($this->cart->getCart(), $orderItem, $this->cartConf); $this->eventDispatcher->dispatch($notifyEvent); - } - // exit with Status Code in TYPO3 v10.4 - if (isset($this->response)) { - $this->response->setStatus(200); - exit(); + $this->logger->info('Payment completed via webhook', ['order_id' => $orderId]); } - return $this->htmlResponse()->withStatus(200); } - protected function restoreCartSession(): void + protected function handlePaymentFailed(array $resource): void { - $cart = $this->cart->getCart(); - $cart->resetOrderNumber(); - $cart->resetInvoiceNumber(); + $orderId = $resource['id'] ?? $resource['order_id'] ?? ''; - // Neue Methode ab TYPO3 10+/Cart 8+: - #$this->sessionHandler->storeCart($cart->getCart(), $this->cartConf['settings']['cart']['pid']); - $this->sessionHandler->writeCart((string)$this->cartConf['settings']['cart']['pid'], $cart); - + if (!$orderId) { + return; + } - // Oder alternativ für ältere Versionen: - // $this->sessionHandler->store($cart, $this->cartConf['settings']['cart']['pid']); - } + $this->loadCartByPaypalOrderId($orderId); - protected function getCurlRequestFromPostData(array $parsePostData): string - { - $curlRequest = 'cmd=_notify-validate'; - foreach ($parsePostData as $key => $value) { - $value = urlencode($value); - $curlRequest .= "&$key=$value"; + if ($this->cart === null) { + return; } - return $curlRequest; + $orderItem = $this->cart->getOrderItem(); + $payment = $orderItem->getPayment(); + + $payment->setStatus('failed'); + $this->paymentRepository->update($payment); + $this->persistenceManager->persistAll(); + + $this->logger->info('Payment failed via webhook', ['order_id' => $orderId]); } - protected function execCurlRequest(string $curlRequest): bool + protected function handlePaymentRefunded(array $resource): void { - $paypalUrl = $this->getPaypalUrl(); + $orderId = $resource['id'] ?? $resource['order_id'] ?? ''; - $ch = curl_init($paypalUrl); - if ($ch === false) { - return false; + if (!$orderId) { + return; } - curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $curlRequest); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); - - if ( - is_array($this->cartPaypalConf) - && array_key_exists('curl_timeout', $this->cartPaypalConf) - && intval($this->cartPaypalConf['curl_timeout']) - ) { - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, intval($this->cartPaypalConf['curl_timeout'])); - } else { - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 300); - } - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Connection: Close']); - - $curlResult = strtolower(curl_exec($ch)); - $curlError = curl_errno($ch); - - if ($curlError !== 0) { - $this->logger->warning( - 'paypal-payment-api', - [ - 'ERROR' => 'Can\'t connect to PayPal to validate IPN message', - 'curl_error' => curl_error($ch), - 'curl_request' => $curlRequest, - 'curl_result' => $curlResult, - ] - ); + $this->loadCartByPaypalOrderId($orderId); - curl_close($ch); - exit; + if ($this->cart === null) { + return; } - if ( - is_array($this->cartPaypalConf) && - array_key_exists('debug', $this->cartPaypalConf) && - $this->cartPaypalConf['debug'] - ) { - $this->logger->debug( - 'paypal-payment-api', - [ - 'curl_info' => curl_getinfo($ch, CURLINFO_HEADER_OUT), - 'curl_request' => $curlRequest, - 'curl_result' => $curlResult, - ] - ); - } + $orderItem = $this->cart->getOrderItem(); + $payment = $orderItem->getPayment(); - $curlResults = explode("\r\n\r\n", $curlResult); + $payment->setStatus('refunded'); + $this->paymentRepository->update($payment); + $this->persistenceManager->persistAll(); - curl_close($ch); + $this->logger->info('Payment refunded via webhook', ['order_id' => $orderId]); + } - return true; + protected function getApiBaseUrl(): string + { + return $this->cartPaypalConf['sandbox'] ? + 'https://api.sandbox.paypal.com' : + 'https://api.paypal.com'; } - protected function getPaypalUrl(): string + protected function restoreCartSession(): void { - if ($this->cartPaypalConf['sandbox']) { - return self::PAYPAL_API_SANDBOX; - } + $cart = $this->cart->getCart(); + $cart->resetOrderNumber(); + $cart->resetInvoiceNumber(); - return self::PAYPAL_API_LIVE; + $this->sessionHandler->writeCart((string)$this->cartConf['settings']['cart']['pid'], $cart); } - protected function loadCartByHash(string $hash, string $type = 'SHash'): void + protected function loadCartByPaypalOrderId(string $paypalOrderId): void { $querySettings = GeneralUtility::makeInstance( Typo3QuerySettings::class @@ -366,14 +527,6 @@ protected function loadCartByHash(string $hash, string $type = 'SHash'): void $querySettings->setStoragePageIds([$this->cartConf['settings']['order']['pid']]); $this->cartRepository->setDefaultQuerySettings($querySettings); - $findOneByMethod = 'findOneBy' . $type; - #$this->cart = $this->cartRepository->findOneBy(['method' => $hash]); - if($type == 'FHash') { - $this->cart = $this->cartRepository->findOneByFHash($hash); - } - else { - $this->cart = $this->cartRepository->findOneBySHash($hash); - } - + $this->cart = $this->cartRepository->findOneByPaypalOrderId($paypalOrderId); } } diff --git a/Classes/EventListener/Order/Payment/ProviderRedirect.php b/Classes/EventListener/Order/Payment/ProviderRedirect.php index c71427c..5a11de1 100644 --- a/Classes/EventListener/Order/Payment/ProviderRedirect.php +++ b/Classes/EventListener/Order/Payment/ProviderRedirect.php @@ -1,32 +1,28 @@ configurationManager = $configurationManager; $this->persistenceManager = $persistenceManager; $this->typoScriptService = $typoScriptService; $this->cartRepository = $cartRepository; + $this->requestFactory = $requestFactory; $this->cartConf = $this->configurationManager->getConfiguration( ConfigurationManager::CONFIGURATION_TYPE_FRAMEWORK, @@ -70,152 +68,250 @@ public function __invoke(PaymentEvent $event): void $this->cartSHash = $cart->getSHash(); $this->cartFHash = $cart->getFHash(); - $this->getQuery(); - - $paymentQueryString = http_build_query($this->paymentQuery); - - #echo $this->getQueryUrl() .' '. $paymentQueryString; - #exit; - header('Location: ' . $this->getQueryUrl() . $paymentQueryString); - exit; - } + try { + $paypalOrder = $this->createPayPalOrder(); - protected function saveCurrentCartToDatabase(): Cart - { - $cart = GeneralUtility::makeInstance(Cart::class); + // Save PayPal order ID to cart + $cart->setPaypalOrderId($paypalOrder['id']); + $this->cartRepository->update($cart); + $this->persistenceManager->persistAll(); - $cart->setOrderItem($this->orderItem); - $cart->setCart($this->cart); - $cart->setPid((int)$this->cartConf['settings']['order']['pid']); + // Redirect to PayPal checkout + $approvalUrl = $this->getApprovalUrl($paypalOrder); - $this->cartRepository->add($cart); - $this->persistenceManager->persistAll(); + header('Location: ' . $approvalUrl); + exit; - return $cart; + } catch (\Exception $e) { + // Log error and redirect to cart with error message + error_log('PayPal order creation failed: ' . $e->getMessage()); + header('Location: /cart?payment_error=1'); + exit; + } } - protected function getQueryUrl(): string + protected function createPayPalOrder(): array { - if ($this->cartPaypalConf['sandbox']) { - return self::PAYPAL_API_SANDBOX; + $accessToken = $this->getAccessToken(); + if (!$accessToken) { + throw new \Exception('Could not get PayPal access token'); } - return self::PAYPAL_API_LIVE; - } + $orderData = $this->buildOrderData(); - protected function getQuery(): void - { - $this->getQueryFromSettings(); - $this->getQueryFromCart(); - $this->getQueryFromOrder(); - } + $apiUrl = $this->getApiBaseUrl() . '/v2/checkout/orders'; - protected function getQueryFromSettings(): void - { - $this->paymentQuery['business'] = $this->cartPaypalConf['business']; - $this->paymentQuery['test_ipn'] = intval($this->cartPaypalConf['sandbox']); + $response = $this->requestFactory->request($apiUrl, 'POST', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $accessToken, + 'Prefer' => 'return=representation' + ], + 'body' => json_encode($orderData) + ]); - $this->paymentQuery['custom'] = $this->cartSHash; - $this->paymentQuery['notify_url'] = $this->getUrl('notify', $this->cartSHash); - $this->paymentQuery['return'] = $this->getUrl('success', $this->cartSHash); - $this->paymentQuery['cancel_return'] = $this->getUrl('cancel', $this->cartFHash); + if ($response->getStatusCode() !== 201) { + throw new \Exception('PayPal order creation failed with status: ' . $response->getStatusCode()); + } - $this->paymentQuery['cmd'] = '_cart'; - $this->paymentQuery['upload'] = '1'; + return json_decode($response->getBody()->getContents(), true); + } - $this->paymentQuery['currency_code'] = $this->orderItem->getCurrencyCode(); + protected function buildOrderData(): array + { + $returnUrl = $this->getUrl('success', $this->cartSHash); + $cancelUrl = $this->getUrl('cancel', $this->cartFHash); + + return [ + 'intent' => 'CAPTURE', + 'purchase_units' => [ + $this->buildPurchaseUnit() + ], + 'application_context' => [ + 'return_url' => $returnUrl, + 'cancel_url' => $cancelUrl, + 'brand_name' => $this->cartPaypalConf['brandName'] ?? 'Your Store', + 'locale' => 'de-DE', + 'landing_page' => 'LOGIN', + 'shipping_preference' => 'NO_SHIPPING', + 'user_action' => 'PAY_NOW' + ] + ]; } - protected function getQueryFromCart(): void + protected function buildPurchaseUnit(): array { - $this->paymentQuery['invoice'] = $this->cart->getOrderNumber(); + $purchaseUnit = [ + 'reference_id' => $this->orderItem->getOrderNumber(), + 'custom_id' => $this->cartSHash, + 'invoice_id' => $this->cart->getOrderNumber(), + 'amount' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($this->cart->getTotalGross(), 2, '.', ''), + 'breakdown' => $this->buildAmountBreakdown() + ] + ]; - if ($this->cartPaypalConf['sendEachItemToPaypal']) { - $this->addEachItemsFromCartToQuery(); + // Add items if configured + if ($this->cartPaypalConf['sendEachItemToPaypal'] ?? true) { + $purchaseUnit['items'] = $this->buildItems(); } else { - $this->addEntireCartToQuery(); + // Single item for entire cart + $purchaseUnit['items'] = [ + [ + 'name' => $this->cartPaypalConf['sendEachItemToPaypalTitle'] ?? 'Order', + 'description' => 'Your order from ' . ($this->cartPaypalConf['brandName'] ?? 'Our Store'), + 'quantity' => '1', + 'unit_amount' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($this->cart->getTotalGross(), 2, '.', '') + ], + 'category' => 'DIGITAL_GOODS' + ] + ]; } + + return $purchaseUnit; } - protected function getQueryFromOrder(): void + protected function buildAmountBreakdown(): array { - $billingAddress = $this->orderItem->getBillingAddress(); + // Berechne Gesamtsteuer durch Summierung der Produktsteuern + $totalTax = 0.0; + foreach ($this->cart->getProducts() as $product) { + $totalTax += $product->getTax(); + } + + $breakdown = [ + 'item_total' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($this->cart->getGross(), 2, '.', '') + ], + 'shipping' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($this->cart->getShipping()->getGross(), 2, '.', '') + ], + 'tax_total' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($totalTax, 2, '.', '') + ], + 'discount' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($this->calculateTotalDiscount(), 2, '.', '') + ] + ]; - $this->paymentQuery['first_name'] = $billingAddress->getFirstName(); - $this->paymentQuery['last_name'] = $billingAddress->getLastName(); - $this->paymentQuery['email'] = $billingAddress->getEmail(); + return $breakdown; } - protected function addEachItemsFromCartToQuery(): void + protected function buildItems(): array { - $shippingGross = $this->cart->getShipping()->getGross(); - $this->paymentQuery['handling_cart'] = number_format( - $shippingGross, - 2, - '.', - '' - ); - - $this->addEachCouponFromCartToQuery(); - $this->addEachProductFromCartToQuery(); + $items = []; + + foreach ($this->cart->getProducts() as $product) { + // Verwende nur verfügbare Methoden + $title = $product->getTitle(); + $quantity = $product->getQuantity(); + + // Für PayPal verwenden wir einfach den Titel als Beschreibung + // da getDescription() und getShortDescription() nicht verfügbar sind + $description = $title; + + $items[] = [ + 'name' => substr($title, 0, 127), + 'description' => substr($description, 0, 127), + 'sku' => $product->getSku() ?? (string)$product->getId(), + 'unit_amount' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($product->getGross() / $quantity, 2, '.', '') + ], + 'tax' => [ + 'currency_code' => $this->orderItem->getCurrencyCode(), + 'value' => number_format($product->getTax() / $quantity, 2, '.', '') + ], + 'quantity' => (string)$quantity, + 'category' => 'PHYSICAL_GOODS' + ]; + } - $this->paymentQuery['mc_gross'] = number_format( - $this->cart->getTotalGross(), - 2, - '.', - '' - ); + return $items; } - protected function addEachCouponFromCartToQuery(): void + protected function calculateTotalDiscount(): float { + $discount = 0.0; + if ($this->cart->getCoupons()) { - $discount = 0; - /** - * @var CartCoupon $cartCoupon - */ - foreach ($this->cart->getCoupons() as $cartCoupon) { - if ($cartCoupon->getIsUseable()) { - $discount += $cartCoupon->getDiscount(); + foreach ($this->cart->getCoupons() as $coupon) { + if ($coupon->getIsUseable()) { + $discount += $coupon->getDiscount(); } } + } + + return $discount; + } - $this->paymentQuery['discount_amount_cart'] = $discount; + protected function getApprovalUrl(array $paypalOrder): string + { + foreach ($paypalOrder['links'] as $link) { + if ($link['rel'] === 'approve') { + return $link['href']; + } } + + throw new \Exception('No approval URL found in PayPal order response'); } - protected function addEachProductFromCartToQuery(): void + protected function getAccessToken(): ?string { - if ($this->orderItem->getProducts()) { - $count = 0; - foreach ($this->orderItem->getProducts() as $productKey => $product) { - $count += 1; - - $this->paymentQuery['item_name_' . $count] = $product->getTitle(); - $this->paymentQuery['quantity_' . $count] = $product->getCount(); - $this->paymentQuery['amount_' . $count] = number_format( - $product->getGross() / $product->getCount(), - 2, - '.', - '' - ); + try { + $authUrl = $this->getApiBaseUrl() . '/v1/oauth2/token'; + $clientId = $this->cartPaypalConf['clientId'] ?? ''; + $clientSecret = $this->cartPaypalConf['clientSecret'] ?? ''; + + if (empty($clientId) || empty($clientSecret)) { + throw new \Exception('PayPal client ID or secret not configured'); } + + $response = $this->requestFactory->request($authUrl, 'POST', [ + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Basic ' . base64_encode($clientId . ':' . $clientSecret), + 'Content-Type' => 'application/x-www-form-urlencoded' + ], + 'body' => 'grant_type=client_credentials' + ]); + + $data = json_decode($response->getBody()->getContents(), true); + + return $data['access_token'] ?? null; + + } catch (\Exception $e) { + error_log('PayPal access token error: ' . $e->getMessage()); + return null; } } - protected function addEntireCartToQuery(): void + protected function getApiBaseUrl(): string { - $this->paymentQuery['quantity'] = 1; - $this->paymentQuery['mc_gross'] = number_format( - $this->cart->getGross() + $this->cart->getServiceGross(), - 2, - '.', - '' - ); + return $this->cartPaypalConf['sandbox'] ? + 'https://api.sandbox.paypal.com' : + 'https://api.paypal.com'; + } + + protected function saveCurrentCartToDatabase(): Cart + { + $cart = GeneralUtility::makeInstance(Cart::class); - $this->paymentQuery['item_name_1'] = $this->cartPaypalConf['sendEachItemToPaypalTitle']; - $this->paymentQuery['quantity_1'] = 1; - $this->paymentQuery['amount_1'] = $this->paymentQuery['mc_gross']; + $cart->setOrderItem($this->orderItem); + $cart->setCart($this->cart); + $cart->setPid((int)$this->cartConf['settings']['order']['pid']); + + $this->cartRepository->add($cart); + $this->persistenceManager->persistAll(); + + return $cart; } protected function getUrl(string $action, string $hash): string @@ -232,7 +328,7 @@ protected function getUrl(string $action, string $hash): string $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); $site = $siteFinder->getSiteByPageId($pid); - + return (string)$site->getRouter()->generateUri( $pid, [ @@ -241,4 +337,4 @@ protected function getUrl(string $action, string $hash): string ] + $arguments ); } -} \ No newline at end of file +} diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index 48de6bd..b3fe879 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -16,9 +16,27 @@ cartPayPal { } plugin.tx_cartpaypal { + # Basis Konfiguration sandbox = 1 - redirectTypeNum = {$plugin.tx_cartpaypal.redirectTypeNum} - sendEachItemToPaypal = 1 + + # PayPal API v2 Credentials (HIER MÜSSEN DIE ECHTEN DATEN EINGETRAGEN WERDEN) + clientId = + clientSecret = + + # Webhook ID für Benachrichtigungen + webhookId = + + # Shop Name der in PayPal angezeigt wird + brandName = Your Store + + # Titel wenn sendEachItemToPaypal = 0 + sendEachItemToPaypalTitle = Order + + # Debug Modus + debug = 0 + + # Timeout für API Requests + curl_timeout = 30 }