diff --git a/.ddev/docker-compose.communicate.yaml b/.ddev/docker-compose.communicate.yaml new file mode 100644 index 00000000..6e222918 --- /dev/null +++ b/.ddev/docker-compose.communicate.yaml @@ -0,0 +1,4 @@ +services: + web: + external_links: + - "ddev-router:myep-next.ddev.site" diff --git a/composer.json b/composer.json index fd447415..e1eb7ada 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "helhum/typo3-console": "^6.4", "kigkonsult/icalcreator": "^2.29", "league/csv": "^9.2", + "league/oauth2-client": "^2.8", "league/period": "^4.9", "lochmueller/staticfilecache": "^12.5", "sjbr/sr-freecap": "^2.6", diff --git a/composer.lock b/composer.lock index ef8e872f..5f4e8634 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f4b99aa9b07e56210c516195545a1d40", + "content-hash": "4f59a07672e06df6976e44c1999be0f2", "packages": [ { "name": "b13/container", @@ -1747,6 +1747,71 @@ ], "time": "2022-01-04T00:13:07+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.5.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" + }, + "time": "2025-02-26T04:37:30+00:00" + }, { "name": "league/period", "version": "4.12.0", diff --git a/config/sites/ep-reisen/config.yaml b/config/sites/ep-reisen/config.yaml index 8e140cc3..bfd4764b 100644 --- a/config/sites/ep-reisen/config.yaml +++ b/config/sites/ep-reisen/config.yaml @@ -255,28 +255,25 @@ routeEnhancers: - 2736 routes: - - routePath: '/{travel_info}/{travel_date}' - _controller: 'Travelinfo::index' + routePath: '/{travel_info}' + _controller: 'Travelinfo::travelInfo' _arguments: - travel_info: travelinfo - travel_date: travelDate + travel_info: travelInfo - routePath: '/{travel_code}' _controller: 'Travelinfo::travelCode' _arguments: travel_code: travelCode - defaultController: 'Travelinfo::index' + defaultController: 'Travelinfo::travelInfo' + requirements: + travel_code: '[A-Z]+\d{6}$' aspects: travel_info: type: PersistedAliasMapper tableName: tx_epproducts_domain_model_travelinfo routeFieldName: path_segment - travel_date: - type: PassthroughMapper travel_code: type: PassthroughMapper - requirements: - travel_date: '\d{4}\-\d{2}\-\d{2}|\d{2}\d{2}\d{4}' PageTypeSuffix: type: PageType default: / diff --git a/public/typo3conf/ext/ep_products/Classes/Controller/TravelinfoController.php b/public/typo3conf/ext/ep_products/Classes/Controller/TravelinfoController.php index a3279963..00156140 100644 --- a/public/typo3conf/ext/ep_products/Classes/Controller/TravelinfoController.php +++ b/public/typo3conf/ext/ep_products/Classes/Controller/TravelinfoController.php @@ -2,10 +2,10 @@ namespace EP\EpProducts\Controller; -use EP\EpProducts\Domain\Model\Date; use EP\EpProducts\Domain\Model\Travelinfo; -use EP\EpProducts\Domain\Repository\DateRepository; use EP\EpProducts\Domain\Repository\TravelinfoRepository; +use EP\EpProducts\MyEP\ApiClient; +use EP\EpProducts\MyEP\ApiException; use TYPO3\CMS\Core\Http\ImmediateResponseException; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; @@ -14,52 +14,34 @@ use TYPO3\CMS\Frontend\Controller\ErrorController; class TravelinfoController extends ActionController { /** - * @var DateRepository + * @var ApiClient */ - private $dateRepository; + private $apiClient; /** * @var TravelinfoRepository */ private $travelinfoRepository; - public function __construct( - DateRepository $dateRepository, - TravelinfoRepository $travelinfoRepository - ) { - $this->dateRepository = $dateRepository; + public function __construct(ApiClient $apiClient, TravelinfoRepository $travelinfoRepository) + { + $this->apiClient = $apiClient; $this->travelinfoRepository = $travelinfoRepository; } public function travelCodeAction(string $travelCode) { - $travelCode = strtoupper($travelCode); - $travelCodeDate = str_replace('-', '/', $travelCode); + // find specific travel info by code including travel date first + $travelInfo = $this->travelinfoRepository->findOneByTravelCode($travelCode); - /** @var Date $date */ - $date = $this->dateRepository->findOneByCode($travelCodeDate); - - if (null === $date) { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $GLOBALS['TYPO3_REQUEST'], - 'Termin nicht gefunden' - ); - throw new ImmediateResponseException($response, 1731071856); + // find general travel info by code without travel date as fallback + if (null === $travelInfo) { + $travelCodeBase = substr($travelCode, 0, -6); + $travelInfo = $this->travelinfoRepository->findOneByTravelCode($travelCodeBase); } - $product = $date->getProduct(); - - if (null === $product) { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $GLOBALS['TYPO3_REQUEST'], - 'Produkt nicht gefunden' - ); - throw new ImmediateResponseException($response, 1731071856); - } - - $travelinfo = $this->travelinfoRepository->findOneByProduct($product); - - if (null === $travelinfo) { + // return 404 in case no travel info is available as last resort + if (null === $travelInfo) { $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( $GLOBALS['TYPO3_REQUEST'], 'Reiseinformationen nicht gefunden' @@ -67,22 +49,39 @@ class TravelinfoController extends ActionController throw new ImmediateResponseException($response, 1731071856); } - $this->view->assign('travelinfo', $travelinfo); - $this->view->assign('date', $date); + $this->handleRequest($travelCode, $travelInfo); } - public function indexAction(Travelinfo $travelinfo, string $travelDate) + public function travelInfoAction(Travelinfo $travelInfo) { - $product = $travelinfo->getProduct(); + $travelCode = $travelInfo->getTravelCode(); - if (preg_match('/^\d{2}\d{2}\d{4}$/', $travelDate)) { - $travelDate = (\DateTimeImmutable::createFromFormat('dmY', $travelDate))->format('Y-m-d'); + $this->handleRequest($travelCode, $travelInfo); + } + + private function handleRequest(string $travelCode, Travelinfo $travelInfo) + { + + // fetch travel data via API call + try { + $travelData = $this->apiClient->getTravel($travelCode); + } catch (ApiException $e) { + $travelData = []; } - /** @var Date $date */ - $date = $this->dateRepository->findForProductAndDate($product, $travelDate)->getFirst(); + // extract courses from additional services + $courses = array_filter($travelData['additionalServices'] ?? [], function ($service) { + return 'KUR' === $service['subType']; + }); - $this->view->assign('travelinfo', $travelinfo); - $this->view->assign('date', $date); + // extract board services from additional services + $board = array_filter($travelData['additionalServices'] ?? [], function ($service) { + return 'VPF' === $service['subType']; + }); + + $this->view->assign('travelInfo', $travelInfo); + $this->view->assign('travelData', $travelData); + $this->view->assign('courses', $courses); + $this->view->assign('board', $board); } } diff --git a/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php new file mode 100644 index 00000000..d1dae47b --- /dev/null +++ b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php @@ -0,0 +1,149 @@ +getHttpClient(); + + try { + $response = $httpClient->request('GET', 'last-update'); + } catch (TransportExceptionInterface $e) { + throw new ApiException($e->getMessage()); + } + + try { + if (200 !== $response->getStatusCode()) { + throw new ApiException($response->getStatusCode()); + } + + $data = $response->toArray(false); + + if (null === $data['timestamp'] ?? null) { + return null; + } + + return new \DateTimeImmutable($data['timestamp']); + } catch (\Throwable $e) { + throw new ApiException($e->getMessage()); + } + } + + /** + * @throws ApiException + */ + public function getPickups(): array + { + $httpClient = $this->getHttpClient(); + + try { + $response = $httpClient->request('GET', 'pickups'); + } catch (TransportExceptionInterface $e) { + throw new ApiException($e->getMessage()); + } + + try { + if (200 !== $response->getStatusCode()) { + throw new ApiException($response->getStatusCode()); + } + + return $response->toArray(false); + } catch (\Throwable $e) { + throw new ApiException($e->getMessage()); + } + } + + /** + * @throws ApiException + */ + public function getTravels(): array + { + $httpClient = $this->getHttpClient(); + + try { + $response = $httpClient->request('GET', 'travels'); + } catch (TransportExceptionInterface $e) { + throw new ApiException($e->getMessage()); + } + + try { + if (200 !== $response->getStatusCode()) { + throw new ApiException($response->getStatusCode()); + } + + return $response->toArray(false); + } catch (\Throwable $e) { + throw new ApiException($e->getMessage()); + } + } + + /** + * @throws ApiException + */ + public function getTravel(string $productCode): array + { + $httpClient = $this->getHttpClient(); + + try { + $response = $httpClient->request('GET', 'travels/' . $productCode); + } catch (TransportExceptionInterface $e) { + throw new ApiException($e->getMessage()); + } + + try { + if (200 !== $response->getStatusCode()) { + throw new ApiException($response->getStatusCode()); + } + + return $response->toArray(false); + } catch (\Throwable $e) { + throw new ApiException($e->getMessage()); + } + } + + private function getHttpClient(): HttpClientInterface + { + $config = GeneralUtility::makeInstance(ExtensionConfiguration::class) + ->get('ep_products'); + + if (null === static::$accessToken || true === static::$accessToken->hasExpired()) { + static::$accessToken = $this + ->getProvider($config) + ->getAccessToken('client_credentials') + ; + } + + return HttpClient::createForBaseUri($config['myEpApiBaseUrl'], [ + 'auth_bearer' => static::$accessToken->getToken(), + ]); + } + + private function getProvider(array $config): AbstractProvider + { + return new GenericProvider([ + 'clientId' => $config['myEpApiClientId'], + 'clientSecret' => $config['myEpApiClientSecret'], + 'redirectUri' => null, + 'urlAuthorize' => $config['myEpApiUrlAuthorize'], + 'urlAccessToken' => $config['myEpApiUrlToken'], + 'urlResourceOwnerDetails' => null, + 'scopes' => 'api', + ]); + } +} diff --git a/public/typo3conf/ext/ep_products/Classes/MyEP/ApiException.php b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiException.php new file mode 100644 index 00000000..b59a70c7 --- /dev/null +++ b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiException.php @@ -0,0 +1,7 @@ + [ - 'basics' => ['showitem' => 'title,path_segment,--linebreak--,product,hide_address,--linebreak--,links, + 'basics' => ['showitem' => 'title,path_segment,--linebreak--,travel_code,hide_address,--linebreak--,links, --linebreak--,teasers'], 'text' => ['showitem' => 'subline,headline,--linebreak--,intro_text,--linebreak--, bus_info_text,--linebreak--, departure_text,--linebreak--,footer_text,--linebreak--,self_arranged_text,--linebreak--,additional_info, @@ -191,32 +191,13 @@ return [ $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] ), ], - 'product' => [ + 'travel_code' => [ 'exclude' => false, - 'label' => 'Produkt', + 'label' => 'Reisecode', 'config' => [ - 'type' => 'group', - 'internal_type' => 'db', - 'allowed' => 'tx_epproducts_domain_model_product', - 'minitems' => 1, - 'maxitems' => 1, - 'size' => 1, - 'fieldControl' => [ - 'elementBrowser' => [ - 'disabled' => true, - ], - ], - 'fieldWizard' => [ - 'recordsOverview' => [ - 'disabled' => true, - ], - ], - 'suggestOptions' => [ - 'default' => [ - 'additionalSearchFields' => 'code,name,keywords', - 'orderBy' => 'code', - ], - ], + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim,required' ], ], 'hide_address' => [ diff --git a/public/typo3conf/ext/ep_products/ext_conf_template.txt b/public/typo3conf/ext/ep_products/ext_conf_template.txt index 6afb2235..04fb2ed8 100644 --- a/public/typo3conf/ext/ep_products/ext_conf_template.txt +++ b/public/typo3conf/ext/ep_products/ext_conf_template.txt @@ -1,3 +1,18 @@ -# customsubcategory=100=General -# cat=epproducts/100/100; type=int; label=Searchresult PID +# cat=General; type=int; label=Searchresult PID searchPageUid = 0 + +# cat=MyEpAPI; type=string; label=MyE&P API base URL (trailing slash!) +myEpApiBaseUrl = + +# cat=MyEpAPI; type=string; label=MyE&P API client id +myEpApiClientId = + +# cat=MyEpAPI; type=string; label=MyE&P API client secret +myEpApiClientSecret = + +# cat=MyEpAPI; type=string; label=MyE&P API authorization URL +myEpApiUrlAuthorize = + +# cat=MyEpAPI; type=string; label=MyE&P API token URL +myEpApiUrlToken = + diff --git a/public/typo3conf/ext/ep_products/ext_localconf.php b/public/typo3conf/ext/ep_products/ext_localconf.php index c35b3d7d..b46c17a7 100644 --- a/public/typo3conf/ext/ep_products/ext_localconf.php +++ b/public/typo3conf/ext/ep_products/ext_localconf.php @@ -252,7 +252,7 @@ $boot = function () { 'EP.ep_products', 'travelinfo', [ - 'Travelinfo' => 'index,travelCode', + 'Travelinfo' => 'travelInfo,travelCode', ] ); diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Layouts/Page/Travelinfo.html b/public/typo3conf/ext/ep_theme/Resources/Private/Layouts/Page/Travelinfo.html index 8c19737a..ead5519d 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Layouts/Page/Travelinfo.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Layouts/Page/Travelinfo.html @@ -7,9 +7,9 @@ - + alt="Bildmarke"/>
- @@ -18,8 +18,8 @@
- +

Adresse

- {date.hotel.name} + {travelData.hotel.name}
- {date.hotel.address -> f:format.nl2br()} + {travelData.hotel.street} +
+ {travelData.hotel.country} {travelData.hotel.city}

- - + + @@ -98,11 +100,11 @@ - +

Eigenanreise

- {travelinfo.selfArrangedText -> f:format.html()} + {travelInfo.selfArrangedText -> f:format.html()}
@@ -114,13 +116,13 @@
- +
- +
@@ -131,11 +133,11 @@
- {travelinfo.additionalInfo -> f:format.html()} + {travelInfo.additionalInfo -> f:format.html()}
- +
@@ -143,18 +145,18 @@
- +

Busbegleitung / Reiseleitung
- {date.guide.name} {date.guide.phone} + {travelData.guide.name} {travelData.guide.phone}

- {travelinfo.importantPhoneNumbers -> f:format.html()} + {travelInfo.importantPhoneNumbers -> f:format.html()}
- +
@@ -162,18 +164,18 @@
- {travelinfo.importantLinks -> f:format.html()} + {travelInfo.importantLinks -> f:format.html()}
- {travelinfo.footerText -> f:format.html()} + {travelInfo.footerText -> f:format.html()}
- +
@@ -196,20 +198,28 @@

{teaser.title}

- + -

- {date.board} -

+ + +
    + +
  • + {item.label} +
  • +
    +
+
+
- +

Folgende Kurse finden statt

    - +
  • {course.label}
  • @@ -257,7 +267,7 @@

    Bitte findet euch mindestens 20 Minuten vor Abfahrt an der gebuchten Haltestelle ein.

    - {travelinfo.busInfoText -> f:format.html()} + {travelInfo.busInfoText -> f:format.html()} - +
    @@ -267,18 +277,18 @@ Datum/Uhrzeit
    {pickup.city}
    {pickup.street}
    - {pickup.date}
    {f:if(condition: pickup.time, then: '{pickup.time} Uhr', else: '???')} + {pickup.time -> f:format.date(format: 'd.m.Y')}
    {pickup.time -> f:format.date(format: 'H:i')} Uhr
    - {travelinfo.departureText -> f:format.html()} + {travelInfo.departureText -> f:format.html()} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/TravelCode.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/TravelCode.html index 816f3889..9015bcde 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/TravelCode.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/TravelCode.html @@ -6,6 +6,6 @@ - + diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/Index.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/TravelInfo.html similarity index 100% rename from public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/Index.html rename to public/typo3conf/ext/ep_theme/Resources/Private/Templates/Travelinfo/TravelInfo.html