diff --git a/CHANGELOG.md b/CHANGELOG.md index ff38a49149..feff76a898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ Changes: * The `0000-00-00 00:00:00` is added for clarity/consistency, as this is probably the default behaviour of your database already. * Removed unused index `consent.deleted_at`. Delete this from your production database if it's there. * Added a specific error page for unsolicited SAML responses (IdP-initiated SSO without a prior AuthnRequest). +* A new parameter `wayf.preferred_idp_entity_ids` must be added to `parameters.yml`. To display a set of IdPs prominent at the top of the WAYF, add the entityId's of those IdPs to this parameter. + * To keep the old behaviour, set the value to `[]` * Stabilized consent checks * In order to make the consent hashes more robust, a more consistent way of hashing the user attributes has been introduced diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index 0eef642d67..0a8c02350b 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -187,6 +187,11 @@ parameters: wayf.display_default_idp_banner_on_wayf: true wayf.default_idp_entity_id: https://default-idp.dev.openconext.local + ## Ordered list of IdP entity IDs to feature prominently at the top of the WAYF. + ## These IdPs appear above the search field and are excluded from the regular searchable list. + ## An empty list means no behaviour change. + wayf.preferred_idp_entity_ids: [] + ## Toggle display & content of global site notice global.site_notice.show: false global.site_notice.allowed.tags: '


    1. ' diff --git a/config/services/ci/controllers.yml b/config/services/ci/controllers.yml index 27a303cbc7..2113eabfcf 100644 --- a/config/services/ci/controllers.yml +++ b/config/services/ci/controllers.yml @@ -19,7 +19,7 @@ services: engineblock.functional_test.controller.wayf: class: OpenConext\EngineBlockFunctionalTestingBundle\Controllers\WayfController arguments: - - '@twig' + - '@OpenConext\EngineBlockBundle\Service\WayfRenderer' engineblock.functional_test.controller.feedback: class: OpenConext\EngineBlockFunctionalTestingBundle\Controllers\FeedbackController diff --git a/config/services/services.yml b/config/services/services.yml index 2d695a5829..d7613ec3f4 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -2,9 +2,20 @@ services: _defaults: public: true + OpenConext\EngineBlock\Service\Wayf\IdpSplitter: + + OpenConext\EngineBlockBundle\Service\WayfViewModelFactory: + arguments: + $wayfExtension: '@OpenConext\EngineBlockBundle\Twig\Extensions\Extension\Wayf' + + OpenConext\EngineBlockBundle\Service\WayfRenderer: + autowire: true + OpenConext\EngineBlockBundle\Bridge\EngineBlockBootstrapper: autowire: true autoconfigure: true + arguments: + $preferredIdpEntityIds: '%wayf.preferred_idp_entity_ids%' tags: - { name: kernel.event_subscriber } diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000000..4059a7eabf --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,52 @@ +# Testing + +## WAYF functional-testing page + +The functional-testing route renders the WAYF page with synthetic IdP data, controllable via query parameters. Use it for manual verification and as the base URL for Cypress tests. + +**Base URL:** `https://engine.dev.openconext.local/functional-testing/wayf` + +### Query parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `lang` | string | `en` | Locale (`en` or `nl`) | +| `connectedIdps` | int | `5` | Number of connected IdPs to generate | +| `unconnectedIdps` | int | `0` | Number of unconnected IdPs to generate | +| `randomIdps` | int | `0` | Generate N IdPs with random (Faker) names instead; overrides connected/unconnected | +| `addDiscoveries` | bool | `true` | Add discovery entries to IdP 1 (gives it 3 list entries instead of 1) | +| `preferredIdpEntityIds[]` | string[] | `[]` | Entity IDs to feature in the preferred section (array syntax required) | +| `defaultIdpEntityId` | string | - | Entity ID of the default IdP (shows banner) | +| `showIdPBanner` | bool | `true` | Whether to show the default IdP banner | +| `displayUnconnectedIdpsWayf` | bool | `false` | Show unconnected IdPs with a "Request access" button | +| `backLink` | bool | `false` | Show "Return to service provider" back link | +| `rememberChoiceFeature` | bool | `false` | Show "Remember my choice" checkbox | +| `cutoffPointForShowingUnfilteredIdps` | int | `100` | Hide the IdP list until the user searches when list length exceeds this value | + +#### Baseline +- [Default (5 connected IdPs)](https://engine.dev.openconext.local/functional-testing/wayf) +- [Dutch locale](https://engine.dev.openconext.local/functional-testing/wayf?lang=nl) +- [10 IdPs](https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=10&addDiscoveries=false) +- [Random IdPs (Faker names)](https://engine.dev.openconext.local/functional-testing/wayf?randomIdps=8) + +#### Cutoff / search +- [Cutoff active - list hidden until search](https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=6&cutoffPointForShowingUnfilteredIdps=5) + +#### Unconnected IdPs / request access +- [Unconnected IdPs visible, no request access](https://engine.dev.openconext.local/functional-testing/wayf?unconnectedIdps=3) +- [Unconnected IdPs + request access button](https://engine.dev.openconext.local/functional-testing/wayf?unconnectedIdps=3&displayUnconnectedIdpsWayf=true) + +#### UI features +- [Back link](https://engine.dev.openconext.local/functional-testing/wayf?backLink=true) +- [Remember my choice](https://engine.dev.openconext.local/functional-testing/wayf?rememberChoiceFeature=true) +- [Default IdP banner](https://engine.dev.openconext.local/functional-testing/wayf?defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F3&showIdPBanner=true&addDiscoveries=false) + +#### Preferred IdPs +- [1 preferred IdP](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&addDiscoveries=false) +- [2 preferred IdPs](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F2&addDiscoveries=false) +- [Preferred = default IdP > banner suppressed](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F1&showIdPBanner=true&addDiscoveries=false) +- [Preferred ≠ default IdP > both visible](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F2&showIdPBanner=true&addDiscoveries=false) +- [Preferred IdP with discoveries (1 IdP > 3 entries)](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1) + + +- [All features enabled](https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=8&unconnectedIdps=2&displayUnconnectedIdpsWayf=true&preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F2&showIdPBanner=true&backLink=true&rememberChoiceFeature=true&addDiscoveries=false) diff --git a/library/EngineBlock/Corto/Module/Service/SingleSignOn.php b/library/EngineBlock/Corto/Module/Service/SingleSignOn.php index 54e2e6233a..3fa431c70c 100644 --- a/library/EngineBlock/Corto/Module/Service/SingleSignOn.php +++ b/library/EngineBlock/Corto/Module/Service/SingleSignOn.php @@ -20,6 +20,7 @@ use OpenConext\EngineBlock\Metadata\Factory\Factory\ServiceProviderFactory; use OpenConext\EngineBlock\Metadata\Discovery; use OpenConext\EngineBlock\Metadata\X509\KeyPairFactory; +use OpenConext\EngineBlock\Service\Wayf\WayfIdp; use OpenConext\EngineBlockBundle\Service\DiscoverySelectionService; use SAML2\AuthnRequest; use SAML2\Constants; @@ -462,8 +463,6 @@ protected function _showWayf(EngineBlock_Saml2_AuthnRequestAnnotationDecorator $ $currentLocale = $container->getLocaleProvider()->getLocale(); - $cookies = $container->getSymfonyRequest()->cookies->all(); - if ($request->isDebugRequest()) { $serviceProvider = $this->getEngineSpRole($this->_server); } else { @@ -477,28 +476,21 @@ protected function _showWayf(EngineBlock_Saml2_AuthnRequestAnnotationDecorator $ $container->getDefaultIdPEntityId() ); - $defaultIdPInIdPList = $this->isDefaultIdPPresent($idpList); - $showDefaultIdpBanner = (bool) $container->shouldDisplayDefaultIdpBannerOnWayf() && $defaultIdPInIdPList; - - $rememberChoiceFeature = $container->getRememberChoice(); - - $output = $this->twig->render( - '@theme/Authentication/View/Proxy/wayf.html.twig', - [ - 'action' => $action, - 'greenHeader' => $serviceProvider->getDisplayName($currentLocale), - 'helpLink' => '/authentication/idp/help-discover?lang=' . $currentLocale, - 'backLink' => $container->isUiOptionReturnToSpActive(), - 'cutoffPointForShowingUnfilteredIdps' => $container->getCutoffPointForShowingUnfilteredIdps(), - 'showIdPBanner' => $showDefaultIdpBanner, - 'rememberChoiceFeature' => $rememberChoiceFeature, - 'showRequestAccess' => $serviceProvider->getCoins()->displayUnconnectedIdpsWayf(), - 'requestId' => $request->getId(), - 'serviceProvider' => $serviceProvider, - 'idpList' => $idpList, - 'cookies' => $cookies, - 'showRequestAccessContainer' => true, - ] + $diContainerRuntime = $application->getDiContainerRuntime(); + + $output = $diContainerRuntime->wayfRenderer->render( + idpList: $idpList, + preferredIdpEntityIds: $diContainerRuntime->getPreferredIdpEntityIds(), + action: $action, + currentLocale: $currentLocale, + defaultIdpEntityId: $container->getDefaultIdPEntityId(), + shouldDisplayBanner: (bool) $container->shouldDisplayDefaultIdpBannerOnWayf(), + backLink: $container->isUiOptionReturnToSpActive(), + cutoffPoint: $container->getCutoffPointForShowingUnfilteredIdps(), + rememberChoice: $container->getRememberChoice(), + showRequestAccess: $serviceProvider->getCoins()->displayUnconnectedIdpsWayf(), + requestId: $request->getId(), + serviceProvider: $serviceProvider, ); $this->_server->sendOutput($output); } @@ -546,21 +538,21 @@ protected function _transformIdpsForWayf(array $idpEntityIds, $isDebugRequest, $ private function buildIdp( ?string $name, string $logo, - $keywords, + array $keywords, string $entityId, bool $isAccessible, bool $isDefaultIdP, ?string $discoveryHash - ): array { - return array( - 'Name' => $name, - 'Logo' => $logo, - 'Keywords' => $keywords, - 'Access' => $isAccessible ? '1' : '0', - 'ID' => md5($entityId), - 'EntityID' => $entityId, - self::IS_DEFAULT_IDP_KEY => $isDefaultIdP, - 'DiscoveryHash' => $discoveryHash, + ): WayfIdp { + return new WayfIdp( + name: $name, + logo: $logo, + keywords: $keywords, + accessible: $isAccessible, + id: md5($entityId), + entityId: $entityId, + isDefaultIdp: $isDefaultIdP, + discoveryHash: $discoveryHash, ); } /** @@ -675,14 +667,4 @@ protected function getEngineSpRole(EngineBlock_Corto_ProxyServer $proxyServer) $serviceProvider = $this->_serviceProviderFactory->createEngineBlockEntityFrom($keyId); return ServiceProvider::fromServiceProviderEntity($serviceProvider); } - - private function isDefaultIdPPresent(array $idpList): bool - { - foreach ($idpList as $idp) { - if ($idp[self::IS_DEFAULT_IDP_KEY] === true) { - return true; - } - } - return false; - } } diff --git a/src/OpenConext/EngineBlock/Service/Wayf/IdpSplitter.php b/src/OpenConext/EngineBlock/Service/Wayf/IdpSplitter.php new file mode 100644 index 0000000000..56b6ebc41b --- /dev/null +++ b/src/OpenConext/EngineBlock/Service/Wayf/IdpSplitter.php @@ -0,0 +1,57 @@ +entityId; + if (isset($orderMap[$entityId])) { + if ($idp->accessible) { + $preferredBuckets[$orderMap[$entityId]][] = $idp; + } + // Unconnected preferred IdPs are excluded from both sections. + } else { + $regular[] = $idp; + } + } + + $mergeArgs = array_values(array_filter($preferredBuckets)); + $preferred = empty($mergeArgs) ? [] : array_merge(...$mergeArgs); + + return new WayfSplitResult(preferred: $preferred, regular: $regular); + } +} diff --git a/src/OpenConext/EngineBlock/Service/Wayf/WayfIdp.php b/src/OpenConext/EngineBlock/Service/Wayf/WayfIdp.php new file mode 100644 index 0000000000..284ae43689 --- /dev/null +++ b/src/OpenConext/EngineBlock/Service/Wayf/WayfIdp.php @@ -0,0 +1,51 @@ + $this->name, + 'Logo' => $this->logo, + 'Keywords' => $this->keywords, + 'Access' => $this->accessible ? '1' : '0', + 'ID' => $this->id, + 'EntityID' => $this->entityId, + 'isDefaultIdp' => $this->isDefaultIdp, + 'DiscoveryHash' => $this->discoveryHash, + ]; + } +} diff --git a/src/OpenConext/EngineBlock/Service/Wayf/WayfSplitResult.php b/src/OpenConext/EngineBlock/Service/Wayf/WayfSplitResult.php new file mode 100644 index 0000000000..dd238d79b6 --- /dev/null +++ b/src/OpenConext/EngineBlock/Service/Wayf/WayfSplitResult.php @@ -0,0 +1,44 @@ +preferred); + } + + public function containsInPreferred(string $entityId): bool + { + return array_any($this->preferred, static fn($idp) => $idp->entityId === $entityId); + } +} diff --git a/src/OpenConext/EngineBlockBundle/Bridge/DiContainerRuntime.php b/src/OpenConext/EngineBlockBundle/Bridge/DiContainerRuntime.php index 77498409d4..b139c7c3b2 100644 --- a/src/OpenConext/EngineBlockBundle/Bridge/DiContainerRuntime.php +++ b/src/OpenConext/EngineBlockBundle/Bridge/DiContainerRuntime.php @@ -18,6 +18,7 @@ namespace OpenConext\EngineBlockBundle\Bridge; +use OpenConext\EngineBlockBundle\Service\WayfRenderer; use Twig\Environment; /** @@ -29,7 +30,15 @@ final readonly class DiContainerRuntime { - public function __construct(public Environment $twig) + public function __construct( + public Environment $twig, + public WayfRenderer $wayfRenderer, + private array $preferredIdpEntityIds = [], + ) { + } + + public function getPreferredIdpEntityIds(): array { + return $this->preferredIdpEntityIds; } } diff --git a/src/OpenConext/EngineBlockBundle/Bridge/EngineBlockBootstrapper.php b/src/OpenConext/EngineBlockBundle/Bridge/EngineBlockBootstrapper.php index bbba86ab23..05824f300c 100644 --- a/src/OpenConext/EngineBlockBundle/Bridge/EngineBlockBootstrapper.php +++ b/src/OpenConext/EngineBlockBundle/Bridge/EngineBlockBootstrapper.php @@ -19,6 +19,7 @@ namespace OpenConext\EngineBlockBundle\Bridge; use EngineBlock_ApplicationSingleton; +use OpenConext\EngineBlockBundle\Service\WayfRenderer; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\KernelEvents; use Twig\Environment; @@ -29,8 +30,10 @@ class EngineBlockBootstrapper implements EventSubscriberInterface public function __construct( Environment $twig, + WayfRenderer $wayfRenderer, + array $preferredIdpEntityIds = [], ) { - $this->diContainerRuntime = new DiContainerRuntime($twig); + $this->diContainerRuntime = new DiContainerRuntime($twig, $wayfRenderer, $preferredIdpEntityIds); } public function onKernelRequest(): void diff --git a/src/OpenConext/EngineBlockBundle/Service/WayfRenderer.php b/src/OpenConext/EngineBlockBundle/Service/WayfRenderer.php new file mode 100644 index 0000000000..7e56308ceb --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Service/WayfRenderer.php @@ -0,0 +1,86 @@ +splitter->split($idpList, $preferredIdpEntityIds); + $showPreferredIdps = $split->hasPreferred(); + $isDefaultIdpPreferred = $split->containsInPreferred($defaultIdpEntityId); + + $showIdPBanner = $shouldDisplayBanner + && $this->isDefaultIdpPresent($idpList) + && (!$showPreferredIdps || !$isDefaultIdpPreferred); + + $viewModel = $this->factory->create( + idpList: $idpList, + regularIdpList: $split->regular, + preferredIdpList: $split->preferred, + showPreferredIdps: $showPreferredIdps, + action: $action, + greenHeader: $serviceProvider->getDisplayName($currentLocale), + helpLink: '/authentication/idp/help-discover?lang=' . $currentLocale, + backLink: $backLink, + cutoffPointForShowingUnfilteredIdps: $cutoffPoint, + showIdPBanner: $showIdPBanner, + rememberChoiceFeature: $rememberChoice, + showRequestAccess: $showRequestAccess, + requestId: $requestId, + serviceProvider: $serviceProvider, + ); + + return $this->twig->render('@theme/Authentication/View/Proxy/wayf.html.twig', $viewModel->toArray()); + } + + private function isDefaultIdpPresent(array $idpList): bool + { + return array_any($idpList, fn(WayfIdp $idp) => $idp->isDefaultIdp); + } +} diff --git a/src/OpenConext/EngineBlockBundle/Service/WayfViewModelFactory.php b/src/OpenConext/EngineBlockBundle/Service/WayfViewModelFactory.php new file mode 100644 index 0000000000..67ae26b195 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Service/WayfViewModelFactory.php @@ -0,0 +1,73 @@ +wayfExtension->getConnectedIdps($idpList), + regularConnectedIdps: $this->wayfExtension->getConnectedIdps($regularIdpList), + preferredConnectedIdps: $this->wayfExtension->getConnectedIdps($preferredIdpList), + showPreferredIdps: $showPreferredIdps, + idpList: $idpList, + regularIdpList: $regularIdpList, + preferredIdpList: $preferredIdpList, + ); + } +} diff --git a/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Locale.php b/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Locale.php index 1e10ebc6e1..126f5bfdaf 100644 --- a/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Locale.php +++ b/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Locale.php @@ -75,13 +75,9 @@ public function getQueryStringFor($locale) $params ); - $query = ''; - foreach ($params as $key => $value) { - $query .= (strlen($query) == 0) ? '?' : '&' ; - $query .= $key. '=' .urlencode($value); - } + $query = http_build_query($params); - return $query; + return strlen($query) > 0 ? '?' . $query : ''; } #[AsTwigFunction(name: 'locale')] diff --git a/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Wayf.php b/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Wayf.php index e66737a8eb..7ba2a4c4b7 100644 --- a/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Wayf.php +++ b/src/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/Wayf.php @@ -19,6 +19,7 @@ namespace OpenConext\EngineBlockBundle\Twig\Extensions\Extension; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; +use OpenConext\EngineBlock\Service\Wayf\WayfIdp; use OpenConext\EngineBlockBundle\Service\IdpHistoryService; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Contracts\Translation\TranslatorInterface; @@ -29,8 +30,6 @@ class Wayf const PREVIOUS_SELECTION_COOKIE_NAME = 'selectedidps'; const REMEMBER_CHOICE_COOKIE_NAME = 'rememberchoice'; - private const ACCESS_ENABLED = '1'; - /** * @var \Symfony\Contracts\Translation\TranslatorInterface */ @@ -80,35 +79,26 @@ private function indexPreviousSelection(): array return array_column($this->previousSelection, null, 'idp'); } - private function formatIdpEntry(array $idp): array + private function formatIdpEntry(WayfIdp $idp): array { - $keywords = $idp['Keywords'] === 'Undefined' - ? [] - : array_values((array)$idp['Keywords']); - - $connected = false; - if (isset($idp['Access']) && $idp['Access'] === self::ACCESS_ENABLED) { - $connected = true; - } + $keywords = $idp->keywords; - // In SingleSignOn.php, the IDP is transformed into an array for the frontend - // Then, here, the array is transformed into another array for the frontend which is actually used in twig return [ - 'entityId' => $idp['EntityID'] ?? null, - 'connected' => $connected, - 'displayTitle' => $idp['Name'] ?? null, - 'title' => strtolower($idp['Name'] ?? ''), - 'keywords' => strtolower(implode('|', $keywords)), - 'logo' => $idp['Logo'] ?? null, - 'isDefaultIdp' => (bool) ($idp['isDefaultIdp'] ?? null), - 'discoveryHash' => $idp['DiscoveryHash'] ?? null, + 'entityId' => $idp->entityId, + 'connected' => $idp->accessible, + 'displayTitle' => $idp->name, + 'title' => strtolower($idp->name ?? ''), + 'keywords' => strtolower(implode('|', $keywords)), + 'logo' => $idp->logo, + 'isDefaultIdp' => $idp->isDefaultIdp, + 'discoveryHash' => $idp->discoveryHash, ]; } private function formatIdpList(array $idpList): array { return array_map( - $this->formatIdpEntry(...), + fn(WayfIdp $idp) => $this->formatIdpEntry($idp), $idpList ); } diff --git a/src/OpenConext/EngineBlockBundle/ViewModel/WayfViewModel.php b/src/OpenConext/EngineBlockBundle/ViewModel/WayfViewModel.php new file mode 100644 index 0000000000..14afdb409c --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/ViewModel/WayfViewModel.php @@ -0,0 +1,66 @@ + $idp->toArray(), $this->idpList); + $vars['regularIdpList'] = array_map(static fn(WayfIdp $idp) => $idp->toArray(), $this->regularIdpList); + $vars['preferredIdpList'] = array_map(static fn(WayfIdp $idp) => $idp->toArray(), $this->preferredIdpList); + return $vars; + } +} diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/WayfController.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/WayfController.php index eaa62604e1..550f3642fb 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/WayfController.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/WayfController.php @@ -18,12 +18,12 @@ namespace OpenConext\EngineBlockFunctionalTestingBundle\Controllers; +use OpenConext\EngineBlockBundle\Service\WayfRenderer; use OpenConext\EngineBlockFunctionalTestingBundle\Helper\TestEntitySeeder; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Twig\Environment; /** * @package OpenConext\EngineBlockFunctionalTestingBundle\Controllers @@ -31,11 +31,9 @@ */ class WayfController extends AbstractController { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private readonly WayfRenderer $wayfRenderer, + ) { } public function wayfAction(Request $request) @@ -48,7 +46,8 @@ public function wayfAction(Request $request) $rememberChoiceFeature = $request->query->getBoolean('rememberChoiceFeature'); $cutoffPointForShowingUnfilteredIdps = $request->query->getInt('cutoffPointForShowingUnfilteredIdps', 100); $showIdPBanner = $request->query->getBoolean('showIdPBanner', true); - $defaultIdpEntityId = $request->query->get('defaultIdpEntityId'); + $defaultIdpEntityId = $request->query->get('defaultIdpEntityId', ''); + $preferredIdpEntityIds = $request->query->all('preferredIdpEntityIds'); $connectedIdps = $request->query->getInt('connectedIdps', 5); $unconnectedIdps = $request->query->getInt('unconnectedIdps'); @@ -58,23 +57,22 @@ public function wayfAction(Request $request) ? TestEntitySeeder::buildIdps($connectedIdps, $unconnectedIdps, $currentLocale, $defaultIdpEntityId, $addDiscoveries) : TestEntitySeeder::buildRandomIdps($randomIdps, $currentLocale, $defaultIdpEntityId); - return new Response($this->twig->render( - '@theme/Authentication/View/Proxy/wayf.html.twig', - [ - 'action' => $this->generateUrl('functional_testing_handle_wayf'), - 'greenHeader' => $currentLocale, - 'helpLink' => '/authentication/idp/help-discover?lang='.$currentLocale, - 'backLink' => $backLink, - 'cutoffPointForShowingUnfilteredIdps' => $cutoffPointForShowingUnfilteredIdps, - 'showIdPBanner' => $showIdPBanner, - 'rememberChoiceFeature' => $rememberChoiceFeature, - 'showRequestAccess' => $displayUnconnectedIdpsWayf, - 'requestId' => 'bogus-request-id', - 'serviceProvider' => TestEntitySeeder::buildSp(), - 'idpList' => $idpList, - 'showRequestAccessContainer' => true, - ] - )); + $output = $this->wayfRenderer->render( + idpList: $idpList, + preferredIdpEntityIds: $preferredIdpEntityIds, + action: $this->generateUrl('functional_testing_handle_wayf'), + currentLocale: $currentLocale, + defaultIdpEntityId: $defaultIdpEntityId, + shouldDisplayBanner: $showIdPBanner, + backLink: $backLink, + cutoffPoint: $cutoffPointForShowingUnfilteredIdps, + rememberChoice: $rememberChoiceFeature, + showRequestAccess: $displayUnconnectedIdpsWayf, + requestId: 'bogus-request-id', + serviceProvider: TestEntitySeeder::buildSp(), + ); + + return new Response($output); } public function handleWayfAction(Request $request) diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Helper/TestEntitySeeder.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Helper/TestEntitySeeder.php index 45785bea7d..d301effeeb 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Helper/TestEntitySeeder.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Helper/TestEntitySeeder.php @@ -23,6 +23,7 @@ use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use OpenConext\EngineBlock\Metadata\Logo; +use OpenConext\EngineBlock\Service\Wayf\WayfIdp; use OpenConext\EngineBlockBundle\Service\DiscoverySelectionService; use Webmozart\Assert\Assert; @@ -188,47 +189,49 @@ public static function buildRandomIdps($numberOfIdps, $locale, $defaultIdpEntity /** * @param array $idpEntityIds * @param string $currentLocale - * @return array[] + * @return WayfIdp[] */ private static function transformIdpsForWayf(array $idpEntityIds, $currentLocale) { $discoveryService = new DiscoverySelectionService(); $identityProviders = self::findIdentityProvidersByEntityId($idpEntityIds); - $wayfIdps = array(); + $wayfIdps = []; foreach ($identityProviders as $identityProvider) { + $isDefaultIdp = $idpEntityIds[$identityProvider->entityId]['isDefaultIdp']; + + // Mirror the production guard: do not show the default IdP in the disconnected section. + if (!$identityProvider->enabledInWayf && $isDefaultIdp) { + continue; + } + $name = 'name' . ucfirst($currentLocale); - $wayfIdp = array( - 'Name' => $identityProvider->$name, - 'Logo' => $identityProvider->logo ? $identityProvider->logo->url : '/images/placeholder.png', - 'Keywords' => $identityProvider->keywordsEn, - 'Access' => ($identityProvider->enabledInWayf) ? '1' : '0', - 'ID' => md5($identityProvider->entityId), - 'EntityID' => $identityProvider->entityId, - 'isDefaultIdp' => $idpEntityIds[$identityProvider->entityId]['isDefaultIdp'], + $wayfIdps[] = new WayfIdp( + name: $identityProvider->$name, + logo: $identityProvider->logo ? $identityProvider->logo->url : '/images/placeholder.png', + keywords: (array) $identityProvider->keywordsEn, + accessible: $identityProvider->enabledInWayf, + id: md5($identityProvider->entityId), + entityId: $identityProvider->entityId, + isDefaultIdp: $isDefaultIdp, + discoveryHash: null, ); - $wayfIdps[] = $wayfIdp; foreach ($identityProvider->getDiscoveries() as $discovery) { - $wayfIdps[] = array( - 'Name' => $discovery->getName($currentLocale), - 'Logo' => $discovery->getLogo() ? $discovery->getLogo()->url : '/images/placeholder.png', - 'Keywords' => $discovery->getKeywords('en'), - 'Access' => ($identityProvider->enabledInWayf) ? '1' : '0', - 'ID' => md5($identityProvider->entityId), - 'EntityID' => $identityProvider->entityId, - 'isDefaultIdp' => $idpEntityIds[$identityProvider->entityId]['isDefaultIdp'], - 'DiscoveryHash' => $discoveryService->hash($discovery), + $wayfIdps[] = new WayfIdp( + name: $discovery->getName($currentLocale), + logo: $discovery->getLogo() ? $discovery->getLogo()->url : '/images/placeholder.png', + keywords: $discovery->getKeywordsArray($currentLocale), + accessible: $identityProvider->enabledInWayf, + id: md5($identityProvider->entityId), + entityId: $identityProvider->entityId, + isDefaultIdp: $isDefaultIdp, + discoveryHash: $discoveryService->hash($discovery), ); } } - $nameSort = static function ($a, $b) { - return strcmp(strtolower($a['Name']), strtolower($b['Name'])); - }; - - // Sort the IdP entries by name - usort($wayfIdps, $nameSort); + usort($wayfIdps, static fn(WayfIdp $a, WayfIdp $b) => strcmp(strtolower($a->name ?? ''), strtolower($b->name ?? ''))); return $wayfIdps; } diff --git a/tests/e2e/cypress/integration/skeune/wayf/wayf.general.spec.js b/tests/e2e/cypress/integration/skeune/wayf/wayf.general.spec.js index 5165d75973..76632774a2 100644 --- a/tests/e2e/cypress/integration/skeune/wayf/wayf.general.spec.js +++ b/tests/e2e/cypress/integration/skeune/wayf/wayf.general.spec.js @@ -202,6 +202,13 @@ context('WAYF behaviour not tied to mouse / keyboard navigation', () => { }); }); + describe('Preferred IdPs section heading', () => { + it('Should show the preferred IdPs section with the correct heading when preferred IdPs are configured', () => { + cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1'); + cy.get('.wayf__preferredIdps').should('be.visible'); + }); + }); + describe('Should show the return to service link when configured', () => { it('Load the page & check if the page is there', () => { cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=5&backLink=true'); diff --git a/tests/e2e/cypress/integration/skeune/wayf/wayf.keyboard.spec.js b/tests/e2e/cypress/integration/skeune/wayf/wayf.keyboard.spec.js index e0da11ad3f..f86e9eed6d 100644 --- a/tests/e2e/cypress/integration/skeune/wayf/wayf.keyboard.spec.js +++ b/tests/e2e/cypress/integration/skeune/wayf/wayf.keyboard.spec.js @@ -63,7 +63,7 @@ context('WAYF when using the keyboard', () => { // todo if html spec is changed, or cypress fixes bug 6207, get rid of the manual focus on search. See https://github.com/cypress-io/cypress/issues/6207 describe('Should be able to traverse the remaining idp section with arrow keys', () => { it('check if pressing down works as expected', () => { - cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1'); + cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1&defaultIdpEntityId=https://example.com/entityId/1'); cy.get(searchFieldSelector).focus(); cy.pressArrowOnIdpList('down', searchFieldClass); cy.pressArrowOnIdpList('down', defaultIdpClass); @@ -78,7 +78,7 @@ context('WAYF when using the keyboard', () => { }); it('check if pressing up works as expected', () => { - cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1'); + cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1&defaultIdpEntityId=https://example.com/entityId/1'); cy.get(searchFieldSelector).focus(); cy.pressArrowOnIdpList('up', searchFieldClass); cy.pressArrowOnIdpList('up', idpClass, '7'); @@ -183,7 +183,7 @@ context('WAYF when using the keyboard', () => { describe('Should have a working default Idp Banner', () => { it('Should have a default Idp banner visible', () => { - cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1'); + cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1&defaultIdpEntityId=https://example.com/entityId/1'); cy.beVisible(defaultIdpSelector); }); diff --git a/tests/e2e/cypress/integration/skeune/wayf/wayf.mouse.spec.js b/tests/e2e/cypress/integration/skeune/wayf/wayf.mouse.spec.js index bb018989de..0df782c1d0 100644 --- a/tests/e2e/cypress/integration/skeune/wayf/wayf.mouse.spec.js +++ b/tests/e2e/cypress/integration/skeune/wayf/wayf.mouse.spec.js @@ -85,7 +85,7 @@ context('WAYF when using the mouse', () => { describe('Should have a working default Idp Banner', () => { it('Should have a default Idp banner visible', () => { - cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1'); + cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?showIdpBanner=1&defaultIdpEntityId=https://example.com/entityId/1'); cy.get(defaultIdpSelector).should('be.visible'); }); diff --git a/tests/e2e/cypress/integration/skeune/wayf/wayf.preferred.spec.js b/tests/e2e/cypress/integration/skeune/wayf/wayf.preferred.spec.js new file mode 100644 index 0000000000..81e51742a4 --- /dev/null +++ b/tests/e2e/cypress/integration/skeune/wayf/wayf.preferred.spec.js @@ -0,0 +1,98 @@ +import { + addAccountButtonSelector, + defaultIdpInformational, + preferredIdpsSectionSelector, + remainingIdpsSearchLabelSelector, +} from '../../../../../../theme/base/javascripts/selectors'; + +const WAYF = 'https://engine.dev.openconext.local/functional-testing/wayf'; +const ENTITY_1 = 'https://example.com/entityId/1'; +const ENTITY_2 = 'https://example.com/entityId/2'; + +const withPreferred = `${WAYF}?preferredIdpEntityIds%5B%5D=${encodeURIComponent(ENTITY_1)}`; +const preferredEqualsDefault = `${WAYF}?defaultIdpEntityId=${encodeURIComponent(ENTITY_1)}&preferredIdpEntityIds%5B%5D=${encodeURIComponent(ENTITY_1)}&showIdPBanner=1`; +const preferredDiffersFromDefault = `${WAYF}?defaultIdpEntityId=${encodeURIComponent(ENTITY_2)}&preferredIdpEntityIds%5B%5D=${encodeURIComponent(ENTITY_1)}&showIdPBanner=1`; + +describe('Search label above regular IdP list', () => { + it('shows search label when preferred IdPs are configured', () => { + cy.visit(withPreferred); + cy.get(remainingIdpsSearchLabelSelector).should('be.visible'); + }); + + it('does not show search label when no preferred IdPs are configured', () => { + cy.visit(WAYF); + cy.get(remainingIdpsSearchLabelSelector).should('not.exist'); + }); +}); + +describe('Preferred IdPs section visibility', () => { + it('shows the preferred section when preferred IdPs are configured', () => { + cy.visit(withPreferred); + cy.get(preferredIdpsSectionSelector).should('be.visible'); + }); + + it('does not show preferred section when no preferred IdPs are configured', () => { + cy.visit(WAYF); + cy.get(preferredIdpsSectionSelector).should('not.exist'); + }); + + it('shows preferred IdP in preferred section and not in remaining list', () => { + cy.visit(withPreferred); + + // entityId/1 appears inside the preferred section + cy.get(`${preferredIdpsSectionSelector} [data-entityid="${ENTITY_1}"]`).should('exist'); + + // entityId/1 must NOT appear in the remaining (non-preferred) IdP list + cy.get(`.wayf__remainingIdps .wayf__idpList:not(.wayf__idpList--preferred) [data-entityid="${ENTITY_1}"]`) + .should('not.exist'); + }); + + it('shows exactly one item in the preferred section when one preferred IdP is configured', () => { + // addDiscoveries=false prevents discovery entries from inflating the preferred count + cy.visit(`${withPreferred}&addDiscoveries=false`); + cy.get(`${preferredIdpsSectionSelector} li`).should('have.length', 1); + }); + + it('shows exactly two items in the preferred section when two preferred IdPs are configured', () => { + const withTwoPreferred = `${WAYF}?preferredIdpEntityIds%5B%5D=${encodeURIComponent(ENTITY_1)}&preferredIdpEntityIds%5B%5D=${encodeURIComponent(ENTITY_2)}&addDiscoveries=false`; + cy.visit(withTwoPreferred); + cy.get(`${preferredIdpsSectionSelector} li`).should('have.length', 2); + }); +}); + +describe('Preferred IdPs with previous selection', () => { + beforeEach(() => { + cy.addOnePreviouslySelectedIdp(false, withPreferred); + }); + + it('hides preferred section when a previous selection is present', () => { + cy.visit(withPreferred); + cy.get(preferredIdpsSectionSelector).should('not.be.visible'); + }); + + it('reveals preferred section after clicking "Use another account"', () => { + cy.visit(withPreferred); + cy.get(addAccountButtonSelector).click({ force: true }); + cy.get(preferredIdpsSectionSelector).should('be.visible'); + }); +}); + +describe('Three display scenarios', () => { + it('scenario 1: no preferred IdPs: banner visible, preferred section absent', () => { + cy.visit(`${WAYF}?showIdPBanner=1&defaultIdpEntityId=${encodeURIComponent(ENTITY_1)}`); + cy.get(defaultIdpInformational).should('be.visible'); + cy.get(preferredIdpsSectionSelector).should('not.exist'); + }); + + it('scenario 3: default IdP is preferred: preferred section visible, banner suppressed', () => { + cy.visit(preferredEqualsDefault); + cy.get(preferredIdpsSectionSelector).should('be.visible'); + cy.get(defaultIdpInformational).should('not.exist'); + }); + + it('scenario 4: preferred IdP differs from default: both preferred section and banner visible', () => { + cy.visit(preferredDiffersFromDefault); + cy.get(preferredIdpsSectionSelector).should('be.visible'); + cy.get(defaultIdpInformational).should('be.visible'); + }); +}); diff --git a/tests/library/EngineBlock/Test/Corto/Module/BindingsTest.php b/tests/library/EngineBlock/Test/Corto/Module/BindingsTest.php index 13cc57dc64..438fb8a14d 100644 --- a/tests/library/EngineBlock/Test/Corto/Module/BindingsTest.php +++ b/tests/library/EngineBlock/Test/Corto/Module/BindingsTest.php @@ -21,6 +21,7 @@ use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use OpenConext\EngineBlockBundle\Bridge\DiContainerRuntime; +use OpenConext\EngineBlockBundle\Service\WayfRenderer; use PHPUnit\Framework\TestCase; use SAML2\Assertion; use SAML2\Assertion\Validation\ConstraintValidator\NotBefore; @@ -59,7 +60,7 @@ public function setUp(): void Phake::when($this->proxyServer)->getConfig('WantsAuthnRequestsSigned')->thenReturn(false); $engineBlock = \EngineBlock_ApplicationSingleton::getInstance(); - $engineBlock->setDiContainerRuntime(new DiContainerRuntime(Phake::mock(Twig\Environment::class))); + $engineBlock->setDiContainerRuntime(new DiContainerRuntime(Phake::mock(Twig\Environment::class), Phake::mock(WayfRenderer::class))); $this->bindings = new EngineBlock_Corto_Module_Bindings($this->proxyServer); } diff --git a/tests/unit/OpenConext/EngineBlock/Service/Wayf/IdpSplitterTest.php b/tests/unit/OpenConext/EngineBlock/Service/Wayf/IdpSplitterTest.php new file mode 100644 index 0000000000..eeebfe86c6 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlock/Service/Wayf/IdpSplitterTest.php @@ -0,0 +1,173 @@ +splitter = new IdpSplitter(); + } + + private function split(array $idpList, array $preferredEntityIds): WayfSplitResult + { + return $this->splitter->split($idpList, $preferredEntityIds); + } + + private function idp(string $entityId, string $access = '1', string $discoveryHash = ''): WayfIdp + { + return new WayfIdp( + name: $entityId, + logo: '', + keywords: [], + accessible: $access === '1', + id: md5($entityId), + entityId: $entityId, + isDefaultIdp: false, + discoveryHash: $discoveryHash !== '' ? $discoveryHash : null, + ); + } + + public function testEmptyPreferredEntityIdsReturnsFullListAsRegular(): void + { + $idpList = [$this->idp('https://idp1.example.org'), $this->idp('https://idp2.example.org')]; + + $result = $this->split($idpList, []); + + $this->assertSame([], $result->preferred); + $this->assertSame($idpList, $result->regular); + } + + public function testPreferredIdpIsMovedOutOfRegularList(): void + { + $idp1 = $this->idp('https://idp1.example.org'); + $idp2 = $this->idp('https://idp2.example.org'); + + $result = $this->split([$idp1, $idp2], ['https://idp1.example.org']); + + $this->assertCount(1, $result->preferred); + $this->assertSame('https://idp1.example.org', $result->preferred[0]->entityId); + $this->assertCount(1, $result->regular); + $this->assertSame('https://idp2.example.org', $result->regular[0]->entityId); + } + + public function testConfiguredOrderIsPreservedInPreferredList(): void + { + $idp1 = $this->idp('https://idp1.example.org'); + $idp2 = $this->idp('https://idp2.example.org'); + $idp3 = $this->idp('https://idp3.example.org'); + + $result = $this->split( + [$idp1, $idp2, $idp3], + ['https://idp3.example.org', 'https://idp1.example.org'] + ); + + $this->assertCount(2, $result->preferred); + $this->assertSame('https://idp3.example.org', $result->preferred[0]->entityId); + $this->assertSame('https://idp1.example.org', $result->preferred[1]->entityId); + $this->assertCount(1, $result->regular); + $this->assertSame('https://idp2.example.org', $result->regular[0]->entityId); + } + + public function testDisconnectedPreferredIdpIsExcludedFromBothLists(): void + { + $idpConnected = $this->idp('https://idp1.example.org', '1'); + $idpDisconnected = $this->idp('https://idp2.example.org', '0'); + + $result = $this->split( + [$idpConnected, $idpDisconnected], + ['https://idp2.example.org'] + ); + + $this->assertSame([], $result->preferred); + // idp1 is not preferred so it stays in regular; idp2 is preferred but disconnected > excluded from both + $this->assertCount(1, $result->regular); + $this->assertSame('https://idp1.example.org', $result->regular[0]->entityId); + } + + public function testMultipleDiscoveryEntriesForSameEntityIdAreGroupedInOrder(): void + { + $idpMain = $this->idp('https://idp1.example.org', '1', ''); + $idpDiscovery = $this->idp('https://idp1.example.org', '1', 'discovery-hash'); + $idpOther = $this->idp('https://idp2.example.org', '1', ''); + + $result = $this->split( + [$idpMain, $idpDiscovery, $idpOther], + ['https://idp1.example.org'] + ); + + $this->assertCount(2, $result->preferred); + $this->assertSame('https://idp1.example.org', $result->preferred[0]->entityId); + $this->assertSame('https://idp1.example.org', $result->preferred[1]->entityId); + $this->assertCount(1, $result->regular); + $this->assertSame('https://idp2.example.org', $result->regular[0]->entityId); + } + + public function testPreferredEntityIdNotInIdpListIsIgnored(): void + { + $idp1 = $this->idp('https://idp1.example.org'); + + $result = $this->split([$idp1], ['https://nonexistent.example.org']); + + $this->assertSame([], $result->preferred); + $this->assertCount(1, $result->regular); + } + + public static function fiveScenarioProvider(): array + { + return [ + 'scenario 1: no preferred, default connected' => [[], true, 'https://default.example.org', false, true], + 'scenario 2: no preferred, default not connected' => [[], false, 'https://default.example.org', false, false], + 'scenario 3: preferred includes default' => [['https://default.example.org'], true, 'https://default.example.org', true, false], + 'scenario 4: preferred does not include default' => [['https://idp1.example.org'], true, 'https://default.example.org', true, true], + 'scenario 5: preferred, default not connected' => [['https://idp1.example.org'], false, 'https://default.example.org', true, false], + ]; + } + + #[DataProvider('fiveScenarioProvider')] + public function testFiveScenarioBannerAndPreferredVisibility( + array $preferredEntityIds, + bool $defaultIdpConnected, + string $defaultIdpEntityId, + bool $expectShowPreferred, + bool $expectShowBanner + ): void { + $idpList = [ + $this->idp('https://idp1.example.org', '1'), + $this->idp('https://default.example.org', $defaultIdpConnected ? '1' : '0'), + ]; + + $split = $this->split($idpList, $preferredEntityIds); + + $showPreferred = $split->hasPreferred(); + $isDefaultPreferred = $split->containsInPreferred($defaultIdpEntityId); + $showBanner = $defaultIdpConnected && (!$showPreferred || !$isDefaultPreferred); + + $this->assertSame($expectShowPreferred, $showPreferred, 'showPreferredIdps mismatch'); + $this->assertSame($expectShowBanner, $showBanner, 'showIdPBanner mismatch'); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/DiContainerRuntimeTest.php b/tests/unit/OpenConext/EngineBlockBundle/DiContainerRuntimeTest.php new file mode 100644 index 0000000000..0735625349 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/DiContainerRuntimeTest.php @@ -0,0 +1,50 @@ +createStub(Environment::class), + $this->createStub(WayfRenderer::class), + ); + + $this->assertSame([], $runtime->getPreferredIdpEntityIds()); + } + + public function testGetPreferredIdpEntityIdsReturnsConfiguredList(): void + { + $entityIds = ['https://idp1.example.org', 'https://idp2.example.org']; + + $runtime = new DiContainerRuntime( + $this->createStub(Environment::class), + $this->createStub(WayfRenderer::class), + $entityIds, + ); + + $this->assertSame($entityIds, $runtime->getPreferredIdpEntityIds()); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Service/WayfRendererTest.php b/tests/unit/OpenConext/EngineBlockBundle/Service/WayfRendererTest.php new file mode 100644 index 0000000000..abef75c547 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Service/WayfRendererTest.php @@ -0,0 +1,265 @@ +factory = $this->createMock(WayfViewModelFactory::class); + $this->twig = $this->createMock(Environment::class); + } + + private function renderer(): WayfRenderer + { + return new WayfRenderer( + factory: $this->factory, + splitter: new IdpSplitter(), + twig: $this->twig, + ); + } + + private function buildViewModel(bool $showIdPBanner = false, bool $showPreferredIdps = false): WayfViewModel + { + $emptyIdps = new ConnectedIdps([], []); + $sp = $this->createStub(ServiceProvider::class); + + return new WayfViewModel( + action: '/sso', + greenHeader: 'SP', + helpLink: '/help', + backLink: false, + cutoffPointForShowingUnfilteredIdps: 100, + showIdPBanner: $showIdPBanner, + rememberChoiceFeature: false, + showRequestAccess: false, + requestId: 'req-1', + serviceProvider: $sp, + connectedIdps: $emptyIdps, + regularConnectedIdps: $emptyIdps, + preferredConnectedIdps: $emptyIdps, + showPreferredIdps: $showPreferredIdps, + idpList: [], + regularIdpList: [], + preferredIdpList: [], + ); + } + + #[DataProvider('bannerConditionProvider')] + public function testBannerConditionPassedToFactory( + bool $shouldDisplayBanner, + bool $defaultIdpInList, + array $preferredIdpEntityIds, + string $defaultIdpEntityId, + bool $expectedShowBanner, + ): void { + $idpList = $defaultIdpInList + ? [new WayfIdp(name: null, logo: '', keywords: [], accessible: true, id: md5($defaultIdpEntityId), entityId: $defaultIdpEntityId, isDefaultIdp: true, discoveryHash: null)] + : [new WayfIdp(name: null, logo: '', keywords: [], accessible: true, id: md5('other'), entityId: 'https://other.example.org', isDefaultIdp: false, discoveryHash: null)]; + + $capturedShowIdPBanner = null; + + $this->factory->expects($this->once()) + ->method('create') + ->willReturnCallback(function () use (&$capturedShowIdPBanner): WayfViewModel { + $namedArgs = func_get_args(); + $capturedShowIdPBanner = $namedArgs[9]; + return $this->buildViewModel($namedArgs[9]); + }); + + $this->twig->method('render')->willReturn(''); + + $sp = $this->createStub(ServiceProvider::class); + $sp->method('getDisplayName')->willReturn('Test SP'); + + $this->renderer()->render( + idpList: $idpList, + preferredIdpEntityIds: $preferredIdpEntityIds, + action: '/sso', + currentLocale: 'en', + defaultIdpEntityId: $defaultIdpEntityId, + shouldDisplayBanner: $shouldDisplayBanner, + backLink: false, + cutoffPoint: 100, + rememberChoice: false, + showRequestAccess: false, + requestId: 'req-1', + serviceProvider: $sp, + ); + + $this->assertSame($expectedShowBanner, $capturedShowIdPBanner); + } + + public static function bannerConditionProvider(): array + { + $defaultId = 'https://default.example.org'; + $otherId = 'https://other.example.org'; + + return [ + 'banner off by config' => [false, true, [], $defaultId, false], + 'banner on, default not in list' => [true, false, [], $defaultId, false], + 'banner on, no preferred IdPs' => [true, true, [], $defaultId, true], + 'banner on, default is preferred (suppressed)' => [true, true, [$defaultId], $defaultId, false], + 'banner on, preferred shown but default is not one of them' => [true, true, [$otherId], $defaultId, true], + ]; + } + + public function testBannerShownWhenDefaultIdpIsInPreferredConfigButDroppedBySpitter(): void + { + $defaultId = 'https://default.example.org'; + $otherId = 'https://other.example.org'; + + // Default IdP is unconnected (splitter will drop it); other preferred IdP is connected. + $idpList = [ + new WayfIdp(name: null, logo: '', keywords: [], accessible: false, id: md5($defaultId), entityId: $defaultId, isDefaultIdp: true, discoveryHash: null), + new WayfIdp(name: null, logo: '', keywords: [], accessible: true, id: md5($otherId), entityId: $otherId, isDefaultIdp: false, discoveryHash: null), + ]; + + $capturedShowIdPBanner = null; + + $this->factory->expects($this->once()) + ->method('create') + ->willReturnCallback(function () use (&$capturedShowIdPBanner): WayfViewModel { + $namedArgs = func_get_args(); + $capturedShowIdPBanner = $namedArgs[9]; + return $this->buildViewModel($namedArgs[9]); + }); + + $this->twig->method('render')->willReturn(''); + + $sp = $this->createStub(ServiceProvider::class); + $sp->method('getDisplayName')->willReturn('Test SP'); + + $this->renderer()->render( + idpList: $idpList, + preferredIdpEntityIds: [$defaultId, $otherId], + action: '/sso', + currentLocale: 'en', + defaultIdpEntityId: $defaultId, + shouldDisplayBanner: true, + backLink: false, + cutoffPoint: 100, + rememberChoice: false, + showRequestAccess: false, + requestId: 'req-1', + serviceProvider: $sp, + ); + + $this->assertTrue($capturedShowIdPBanner, 'Banner should show: default IdP was in preferred config but dropped by splitter, so it is not visible anywhere'); + } + + + public function testSplitsIdpListBeforePassingToFactory(): void + { + $preferredId = 'https://preferred.example.org'; + $regularId = 'https://regular.example.org'; + + $idpList = [ + new WayfIdp(name: null, logo: '', keywords: [], accessible: true, id: md5($preferredId), entityId: $preferredId, isDefaultIdp: false, discoveryHash: null), + new WayfIdp(name: null, logo: '', keywords: [], accessible: true, id: md5($regularId), entityId: $regularId, isDefaultIdp: false, discoveryHash: null), + ]; + + $capturedRegular = null; + $capturedPreferred = null; + + $this->factory->expects($this->once()) + ->method('create') + ->willReturnCallback(function ( + array $idpList, + array $regularIdpList, + array $preferredIdpList, + ) use ( + &$capturedRegular, + &$capturedPreferred + ): WayfViewModel { + $capturedRegular = $regularIdpList; + $capturedPreferred = $preferredIdpList; + return $this->buildViewModel(); + }); + + $this->twig->method('render')->willReturn(''); + + $sp = $this->createStub(ServiceProvider::class); + $sp->method('getDisplayName')->willReturn('Test SP'); + + $this->renderer()->render( + idpList: $idpList, + preferredIdpEntityIds: [$preferredId], + action: '/sso', + currentLocale: 'en', + defaultIdpEntityId: '', + shouldDisplayBanner: false, + backLink: false, + cutoffPoint: 100, + rememberChoice: false, + showRequestAccess: false, + requestId: 'req-1', + serviceProvider: $sp, + ); + + $this->assertCount(1, $capturedPreferred); + $this->assertSame($preferredId, $capturedPreferred[0]->entityId); + $this->assertCount(1, $capturedRegular); + $this->assertSame($regularId, $capturedRegular[0]->entityId); + } + + public function testReturnsRenderedHtml(): void + { + $expectedHtml = 'WAYF'; + + $this->factory->method('create')->willReturn($this->buildViewModel()); + $this->twig->method('render')->willReturn($expectedHtml); + + $sp = $this->createStub(ServiceProvider::class); + $sp->method('getDisplayName')->willReturn('SP'); + + $result = $this->renderer()->render( + idpList: [], + preferredIdpEntityIds: [], + action: '/sso', + currentLocale: 'en', + defaultIdpEntityId: '', + shouldDisplayBanner: false, + backLink: false, + cutoffPoint: 100, + rememberChoice: false, + showRequestAccess: false, + requestId: 'req-1', + serviceProvider: $sp, + ); + + $this->assertSame($expectedHtml, $result); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/WayfTest.php b/tests/unit/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/WayfTest.php index a346324a71..3b732a43d4 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/WayfTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Twig/Extensions/Extension/WayfTest.php @@ -19,6 +19,7 @@ namespace Tests\OpenConext\EngineBlockBundle\Twig\Extensions\Extension; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; +use OpenConext\EngineBlock\Service\Wayf\WayfIdp; use OpenConext\EngineBlockBundle\Twig\Extensions\Extension\ConnectedIdps; use OpenConext\EngineBlockBundle\Twig\Extensions\Extension\Wayf; use PHPUnit\Framework\Attributes\DataProvider; @@ -43,24 +44,26 @@ protected function setUp(): void public function testGetConnectedIdpsWithEmptyPreviousSelection() { $idpList = [ - [ - 'EntityID' => 'https://idp1.example.org', - 'Access' => '1', - 'Name' => 'IDP One', - 'Keywords' => ['university', 'education'], - 'Logo' => 'logo1.png', - 'isDefaultIdp' => false, - 'DiscoveryHash' => 'hash1' - ], - [ - 'EntityID' => 'https://idp2.example.org', - 'Access' => '0', - 'Name' => 'IDP Two', - 'Keywords' => 'Undefined', - 'Logo' => 'logo2.png', - 'isDefaultIdp' => true, - 'DiscoveryHash' => 'hash2' - ] + new WayfIdp( + name: 'IDP One', + logo: 'logo1.png', + keywords: ['university', 'education'], + accessible: true, + id: md5('https://idp1.example.org'), + entityId: 'https://idp1.example.org', + isDefaultIdp: false, + discoveryHash: 'hash1', + ), + new WayfIdp( + name: 'IDP Two', + logo: 'logo2.png', + keywords: [], + accessible: false, + id: md5('https://idp2.example.org'), + entityId: 'https://idp2.example.org', + isDefaultIdp: true, + discoveryHash: 'hash2', + ), ]; $result = $this->wayf->getConnectedIdps($idpList); @@ -118,24 +121,26 @@ public function testGetConnectedIdpsWithPreviousSelection(string $storedCookieVa $wayf = new Wayf($requestStack, $this->translator); $idpList = [ - [ - 'EntityID' => 'https://idp1.example.org', - 'Access' => '1', - 'Name' => 'IDP One', - 'Keywords' => ['university', 'education'], - 'Logo' => 'logo1.png', - 'isDefaultIdp' => false, - 'DiscoveryHash' => '' - ], - [ - 'EntityID' => 'https://idp1.example.org', - 'Access' => '1', - 'Name' => 'IDP One Discovery', - 'Keywords' => [], - 'Logo' => 'logo2.png', - 'isDefaultIdp' => false, - 'DiscoveryHash' => 'hash1' - ] + new WayfIdp( + name: 'IDP One', + logo: 'logo1.png', + keywords: ['university', 'education'], + accessible: true, + id: md5('https://idp1.example.org'), + entityId: 'https://idp1.example.org', + isDefaultIdp: false, + discoveryHash: null, + ), + new WayfIdp( + name: 'IDP One Discovery', + logo: 'logo2.png', + keywords: [], + accessible: true, + id: md5('https://idp1.example.org'), + entityId: 'https://idp1.example.org', + isDefaultIdp: false, + discoveryHash: 'hash1', + ), ]; $result = $wayf->getConnectedIdps($idpList); @@ -203,7 +208,8 @@ public function testGetWayfJsonConfig() $this->translator->method('trans') ->willReturnMap([ ['more_idp_results', [], null, null, 'More results'], - ['request_access', [], null, null, 'Request Access'] + ['request_access', [], null, null, 'Request Access'], + ['wayf_remaining_idps_search_label', [], null, null, 'Or search for a Dutch institution from the list'], ]); // Test with showRequestAccess = true diff --git a/theme/base/javascripts/selectors.js b/theme/base/javascripts/selectors.js index d597029c68..d5bfdca7c0 100644 --- a/theme/base/javascripts/selectors.js +++ b/theme/base/javascripts/selectors.js @@ -77,6 +77,8 @@ export const idpDeleteDisabledSelector = `.${idpDeleteDisabledClass}`; export const remainingIdpSectionSelector = '.wayf__remainingIdps'; export const remainingIdpListSelector = '.wayf__remainingIdps .wayf__idpList'; export const remainingIdpSelector = '.wayf__remainingIdps .wayf__idpList .wayf__idp'; +export const preferredIdpsSectionSelector = '.wayf__preferredIdps'; +export const remainingIdpsSearchLabelSelector = '.remainingIdps__searchLabel'; export const searchFieldClass = 'search__field'; export const searchFieldSelector = `.${searchFieldClass}`; export const searchResetClass = 'search__reset'; diff --git a/theme/base/javascripts/wayf/utility/switchIdpSection.js b/theme/base/javascripts/wayf/utility/switchIdpSection.js index 7d90a55e60..90ee9d6cbe 100644 --- a/theme/base/javascripts/wayf/utility/switchIdpSection.js +++ b/theme/base/javascripts/wayf/utility/switchIdpSection.js @@ -1,6 +1,6 @@ import {toggleVisibility} from '../../utility/toggleVisibility'; import {focusOn} from "../../utility/focusOn"; -import {noAccessSectionSelector, noResultSectionSelector, remainingIdpSectionSelector, searchFieldSelector, selectedIdpsSectionSelector} from '../../selectors'; +import {noAccessSectionSelector, noResultSectionSelector, preferredIdpsSectionSelector, remainingIdpSectionSelector, searchFieldSelector, selectedIdpsSectionSelector} from '../../selectors'; import {isVisibleElement} from '../../utility/isVisibleElement'; export const switchIdpSection = () => { @@ -8,10 +8,14 @@ export const switchIdpSection = () => { const previousIdps = document.querySelector(selectedIdpsSectionSelector); const noResults = document.querySelector(noResultSectionSelector); const noAccess = document.querySelector(noAccessSectionSelector); + const preferredIdps = document.querySelector(preferredIdpsSectionSelector); const ariaHidden = 'aria-hidden'; toggleVisibility(previousIdps); toggleVisibility(remainingIdps); + if (preferredIdps) { + toggleVisibility(preferredIdps); + } remainingIdps.removeAttribute(ariaHidden); diff --git a/theme/base/stylesheets/pages/wayf.scss b/theme/base/stylesheets/pages/wayf.scss index 8668f88a15..2c6cca75a1 100644 --- a/theme/base/stylesheets/pages/wayf.scss +++ b/theme/base/stylesheets/pages/wayf.scss @@ -10,3 +10,4 @@ @use 'wayf/noResults'; @use 'wayf/previousSelection'; @use 'wayf/remainingIdps'; +@use 'wayf/preferredIdps'; diff --git a/theme/base/stylesheets/pages/wayf/preferredIdps.scss b/theme/base/stylesheets/pages/wayf/preferredIdps.scss new file mode 100644 index 0000000000..54e46f029d --- /dev/null +++ b/theme/base/stylesheets/pages/wayf/preferredIdps.scss @@ -0,0 +1,14 @@ +@use "../../variables"; + +.wayf__preferredIdps { + margin-bottom: 0; + margin-top: 2rem; +} + +.remainingIdps__searchLabel { + color: variables.$brownishGray; + font-family: Nunito, sans-serif; + font-size: variables.$f-xl; + font-weight: variables.$bolder; + margin: 2rem 0 0; +} diff --git a/theme/base/stylesheets/pages/wayf/remainingIdps.scss b/theme/base/stylesheets/pages/wayf/remainingIdps.scss index 5cb65f41d7..38196c01c9 100644 --- a/theme/base/stylesheets/pages/wayf/remainingIdps.scss +++ b/theme/base/stylesheets/pages/wayf/remainingIdps.scss @@ -68,6 +68,10 @@ } } + > .remainingIdps__searchLabel + .wayf__search { + margin-top: 0.5rem; + } + > .remainingIdps__defaultIdp { @include mixins-skeune.informational; margin: 1.25rem 0 1.25rem; diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/backLink.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/backLink.html.twig index 65b88aecd6..5af6e094d8 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/backLink.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/backLink.html.twig @@ -1,9 +1,9 @@ -{% if backLink is defined and backLink == true %} +{% if backLink %} {% endif %} diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idp.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idp.html.twig index 76ea916c62..a59b655bc4 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idp.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idp.html.twig @@ -30,9 +30,16 @@ id="idp__title{{ listName }}{{ loop.index }}" class="idp__title" >{{ 'wayf_idp_title_screenreader'|trans }}{{ idp['displayTitle'] }}
      - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpForm.html.twig' %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpForm.html.twig' with { + idp: idp, + action: action, + requestId: requestId, + } only %} {% if idp['connected'] is defined and not idp['connected'] or delete %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpDeleteDisable.html.twig' %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpDeleteDisable.html.twig' with { + idp: idp, + delete: delete, + } only %} {% endif %}
      diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpForm.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpForm.html.twig index 61a6c140b3..2eb57a719f 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpForm.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpForm.html.twig @@ -2,8 +2,8 @@ - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpSubmitButton.html.twig' with { hidden: true } %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpSubmitButton.html.twig' with { hidden: true } only %} diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig index cc222b0ed3..2a53813333 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig @@ -22,5 +22,12 @@ data-connectable="{{ connectable }}" data-url="{{ logoUrl }}" > - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idp.html.twig' with { idp: idp } %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idp.html.twig' with { + idp: idp, + loop: loop, + listName: listName, + delete: delete, + action: action, + requestId: requestId, + } only %}
    2. diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig index b21ba191a0..d0608a2bd3 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig @@ -10,15 +10,27 @@ {# First show all connected Idps #} {% for idp in idpList %} {% if idp['connected'] is defined and idp['connected'] %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig' with { idp: idp } - %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig' with { + idp: idp, + loop: loop, + delete: delete, + listName: listName, + action: action, + requestId: requestId, + } only %} {% endif %} {% endfor %} - {# Next show all unconnnected Idps #} + {# Next show all unconnected Idps #} {% for idp in idpList %} {% if showRequestAccess and idp['connected'] is defined and not idp['connected'] %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig' with { idp: idp } - %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig' with { + idp: idp, + loop: loop, + delete: delete, + listName: listName, + action: action, + requestId: requestId, + } only %} {% endif %} {% endfor %}
diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/noAccess.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/noAccess.html.twig index f0341bfc0b..183f06d79d 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/noAccess.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/noAccess.html.twig @@ -5,6 +5,8 @@ {# placeholder for the idp without access #} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/helpdesk.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/requestForm.html.twig' %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/helpdesk.html.twig' only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/requestForm.html.twig' with { + serviceProvider: serviceProvider, + } only %} diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/requestForm.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/requestForm.html.twig index 238e28be63..8f5a873123 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/requestForm.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/noAccess/requestForm.html.twig @@ -56,5 +56,5 @@ - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/ctas.html.twig' %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/ctas.html.twig' only %} diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/preferredIdps.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/preferredIdps.html.twig new file mode 100644 index 0000000000..c5b25cc0b3 --- /dev/null +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/preferredIdps.html.twig @@ -0,0 +1,7 @@ +
+
    + {% for idp in preferredConnectedIdps.formattedIdpList %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpItem.html.twig' with { idp: idp, delete: false, listName: 'preferred', loop: loop, action: action, requestId: requestId } only %} + {% endfor %} +
+
diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/preselection.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/preselection.html.twig index f8489a3134..24d382df43 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/preselection.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/preselection.html.twig @@ -13,10 +13,20 @@ {% endapply %}
- {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig' with { idpList: previousSelectionList, delete: true, listName: 'preselection', id: 'previousSelection__title' } %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig' with { + idpList: previousSelectionList, + delete: true, + listName: 'preselection', + id: 'previousSelection__title', + showIdPBanner: false, + showRequestAccess: showRequestAccess, + cutoffPointForShowingUnfilteredIdps: cutoffPointForShowingUnfilteredIdps, + action: action, + requestId: requestId, + } only %} diff --git a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/remainingIdps.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/remainingIdps.html.twig index 8ac292aa30..5c59895c7e 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/remainingIdps.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/Partials/WAYF/remainingIdps.html.twig @@ -6,10 +6,16 @@ {{ 'wayf_select_account_screenreader'|trans }} {% endif %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/search.html.twig' %} + {% if showPreferredIdps %} +

{{ 'wayf_remaining_idps_search_label'|trans }}

+ {% endif %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/search.html.twig' with { + action: action, + requestId: requestId, + } only %} - {% set idpListSorted = connectedIdps.formattedIdpList|sort((a, b) => a.displayTitle|lower <=> b.displayTitle|lower) %} - {% if showIdPBanner is defined and showIdPBanner %} + {% set idpListSorted = regularConnectedIdps.formattedIdpList|sort((a, b) => a.displayTitle|lower <=> b.displayTitle|lower) %} + {% if showIdPBanner %} {% set requestUri %} {% if '?' in app.request.requestUri %} {{ app.request.requestUri|replace({'?': '#defaultIdp?'}) }} @@ -25,16 +31,32 @@ class: 'remainingIdps__defaultIdp', text: defaultIdpText, id: 'defaultIdpDescription', - } %} - {% endif %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/rememberChoice.html.twig' %} - {% if showIdPBanner is defined and showIdPBanner %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig' with { idpList: idpListSorted, delete: false, listName: 'remaining', id: 'remainingIdps__title', showIdPBanner: showIdPBanner } %} - {% else %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig' with { idpList: idpListSorted, delete: false, listName: 'remaining', id: 'remainingIdps__title', showIdPBanner: false } %} + } only %} {% endif %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/rememberChoice.html.twig' with { + action: action, + rememberChoiceFeature: rememberChoiceFeature, + } only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/idp/idpList.html.twig' with { + idpList: idpListSorted, + delete: false, + listName: 'remaining', + id: 'remainingIdps__title', + showIdPBanner: showIdPBanner, + showRequestAccess: showRequestAccess, + cutoffPointForShowingUnfilteredIdps: cutoffPointForShowingUnfilteredIdps, + action: action, + requestId: requestId, + } only %} diff --git a/theme/base/templates/modules/Authentication/View/Proxy/wayf.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/wayf.html.twig index ebda547252..89735794de 100644 --- a/theme/base/templates/modules/Authentication/View/Proxy/wayf.html.twig +++ b/theme/base/templates/modules/Authentication/View/Proxy/wayf.html.twig @@ -6,11 +6,60 @@ {% set organisationNoun = 'organisation_noun'|trans %} {% set pageTitle = 'log_in_to'|trans({'%arg1%': spName, '%organisationNoun%': organisationNoun}) %} -{# Data object containing the formatted IdP's #} -{% set connectedIdps = connectedIdps(idpList) %} - {% block title %}{{ parent() }} - {{ pageTitle }} {% endblock %} {% block content %} - {% include '@theme/Authentication/View/Proxy/wayf_content.html.twig' %} + {% include '@theme/Default/Partials/LoginBar.html.twig' with { loginName: 'suite_name'|trans } only %} +
+ {% include '@theme/Authentication/View/Proxy/Partials/Shared/header.html.twig' with { pageTitle: pageTitle } only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noScript.html.twig' only %} + + {% block wayf_content %} +
+ {% include '@theme/Authentication/View/Proxy/Partials/Shared/site-notice.html.twig' only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/successMessage.html.twig' only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/preselection.html.twig' with { + connectedIdps: connectedIdps, + action: action, + requestId: requestId, + showRequestAccess: showRequestAccess, + cutoffPointForShowingUnfilteredIdps: cutoffPointForShowingUnfilteredIdps, + } only %} + {% if showPreferredIdps %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/preferredIdps.html.twig' with { + connectedIdps: connectedIdps, + preferredConnectedIdps: preferredConnectedIdps, + action: action, + requestId: requestId, + } only %} + {% endif %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/remainingIdps.html.twig' with { + connectedIdps: connectedIdps, + regularConnectedIdps: regularConnectedIdps, + showIdPBanner: showIdPBanner, + showPreferredIdps: showPreferredIdps, + action: action, + requestId: requestId, + rememberChoiceFeature: rememberChoiceFeature, + cutoffPointForShowingUnfilteredIdps: cutoffPointForShowingUnfilteredIdps, + showRequestAccess: showRequestAccess, + } only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/backLink.html.twig' with { + backLink: backLink, + } only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/noAccess.html.twig' with { + serviceProvider: serviceProvider, + } only %} + {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noResults.html.twig' only %} +
+ {% endblock %} +
+ {% include '@theme/Authentication/View/Proxy/Partials/WAYF/scriptConfig.html.twig' with { + connectedIdps: connectedIdps, + serviceProvider: serviceProvider, + showRequestAccess: showRequestAccess, + rememberChoiceFeature: rememberChoiceFeature, + cutoffPointForShowingUnfilteredIdps: cutoffPointForShowingUnfilteredIdps, + } only %} + {% include '@theme/Default/Partials/footer.html.twig' with { helpLink: helpLink } only %} {% endblock %} diff --git a/theme/base/templates/modules/Authentication/View/Proxy/wayf_content.html.twig b/theme/base/templates/modules/Authentication/View/Proxy/wayf_content.html.twig deleted file mode 100644 index 8c835488e8..0000000000 --- a/theme/base/templates/modules/Authentication/View/Proxy/wayf_content.html.twig +++ /dev/null @@ -1,19 +0,0 @@ -{% include '@theme/Default/Partials/LoginBar.html.twig' with { loginName: 'suite_name'|trans } %} -
- {% include '@theme/Authentication/View/Proxy/Partials/Shared/header.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noScript.html.twig' %} - - {% block wayf_content %} -
- {% include '@theme/Authentication/View/Proxy/Partials/Shared/site-notice.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/successMessage.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/preselection.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/remainingIdps.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/backLink.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noAccess/noAccess.html.twig' %} - {% include '@theme/Authentication/View/Proxy/Partials/WAYF/noResults.html.twig' %} -
- {% endblock %} -
-{% include '@theme/Authentication/View/Proxy/Partials/WAYF/scriptConfig.html.twig' %} -{% include '@theme/Default/Partials/footer.html.twig' %} diff --git a/theme/openconext/templates/layouts/scripts/default.html.twig b/theme/openconext/templates/layouts/scripts/default.html.twig index 9119bc008a..6510d51438 100644 --- a/theme/openconext/templates/layouts/scripts/default.html.twig +++ b/theme/openconext/templates/layouts/scripts/default.html.twig @@ -111,9 +111,7 @@ {% endif %} -{% if showRequestAccessContainer is defined %} -
-{% endif %} +
diff --git a/theme/openconext/templates/modules/Authentication/View/Proxy/wayf.html.twig b/theme/openconext/templates/modules/Authentication/View/Proxy/wayf.html.twig index 7f713190b7..03dfeeb744 100644 --- a/theme/openconext/templates/modules/Authentication/View/Proxy/wayf.html.twig +++ b/theme/openconext/templates/modules/Authentication/View/Proxy/wayf.html.twig @@ -3,9 +3,6 @@ {# Prepare the page title #} {% set pageTitle = 'log_in_to'|trans %} -{# Data object containing the formatted IdP's #} -{% set connectedIdps = connectedIdps(idpList) %} - {% block title %}{{ parent() }} - {{ pageTitle }} {% endblock %} {% block pageHeading %}{{ parent() }} - {{ pageTitle }}{% endblock %} diff --git a/theme/skeune/translations/messages.en.php b/theme/skeune/translations/messages.en.php index 5554b44af0..39f4ae13d6 100644 --- a/theme/skeune/translations/messages.en.php +++ b/theme/skeune/translations/messages.en.php @@ -59,6 +59,7 @@ 'wayf_noaccess_form_announcement_screenreader' => 'Some required fields are not filled in, or not correctly filled in.', 'wayf_defaultIdp_start' => 'If your %organisation_noun% is not listed,', 'wayf_defaultIdp_linkText' => '%defaultIdpName% is available as an alternative.', + 'wayf_remaining_idps_search_label' => 'Or search for a Dutch institution from the list', 'wayf_idp_title_screenreader' => 'Login with ', 'wayf_idp_title_noaccess_screenreader' => 'No access with', diff --git a/theme/skeune/translations/messages.nl.php b/theme/skeune/translations/messages.nl.php index f3739827d2..f76f9940e1 100644 --- a/theme/skeune/translations/messages.nl.php +++ b/theme/skeune/translations/messages.nl.php @@ -58,6 +58,7 @@ 'wayf_noaccess_form_announcement_screenreader' => 'Er zijn verplichte velden niet, of niet goed ingevuld.', 'wayf_defaultIdp_start' => 'Als je %organisation_noun% niet in de lijst staat,', 'wayf_defaultIdp_linkText' => 'is %defaultIdpName% beschikbaar als alternatief.', + 'wayf_remaining_idps_search_label' => 'Of zoek een Nederlandse instelling uit de lijst', 'wayf_idp_title_screenreader' => 'Inloggen met ', 'wayf_idp_title_noaccess_screenreader' => 'Geen toegang met', diff --git a/theme/skeune/translations/messages.pt.php b/theme/skeune/translations/messages.pt.php index 7d08062275..60b21b8141 100644 --- a/theme/skeune/translations/messages.pt.php +++ b/theme/skeune/translations/messages.pt.php @@ -59,6 +59,7 @@ 'wayf_noaccess_form_announcement_screenreader' => 'Some required fields are not filled in, or not correctly filled in.', 'wayf_defaultIdp_start' => 'If your %organisation_noun% is not listed,', 'wayf_defaultIdp_linkText' => '%defaultIdpName% is available as an alternative.', + 'wayf_remaining_idps_search_label' => 'Ou procure uma instituição neerlandesa na lista', 'wayf_idp_title_screenreader' => 'Login with ', 'wayf_idp_title_noaccess_screenreader' => 'No access with',