Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/css/components/modal-window.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
display: flex;
justify-content: space-between;
margin-top: calculateRem(45px);

&--single {
justify-content: flex-end;
}
}

.modalWindowButton {
Expand Down
23 changes: 23 additions & 0 deletions assets/js/modalWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ window.addEventListener('load', function () {
}
}

// Modals rendered with `autoOpen: true` (see helpers/modal.html.twig) should show
// themselves immediately, e.g. to give feedback after a redirect back from another
// service. The query parameter that signalled the result is stripped from the URL
// first so a page refresh does not reopen the modal. This has to happen before the
// trigger is clicked: Firefox resolves the trigger's fragment link against the URL
// as it was at click time, and would otherwise reload the page with the original
// query string, over and over.
const autoOpenButtons = document.querySelectorAll('.modalWindowButton[data-auto-open="true"]');
if (!!autoOpenButtons) {
for (let i = 0; i < autoOpenButtons.length; i++) {
const trigger = autoOpenButtons[i];

const cleanParam = trigger.getAttribute('data-clean-param');
if (cleanParam) {
const url = new URL(window.location.href);
url.searchParams.delete(cleanParam);
window.history.replaceState(null, '', url.pathname + url.search + url.hash);
Comment thread
johanib marked this conversation as resolved.
}

trigger.click();
}
}

const cancelButtons = document.querySelectorAll('.modalWindow__cancel');
if (!!cancelButtons) {
for (let i = 0; i < cancelButtons.length; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

use OpenConext\ProfileBundle\Service\UserService;
use OpenConext\ProfileBundle\Service\WayfResetLinkBuilder;
use OpenConext\ProfileBundle\Service\WayfResetResultResolver;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
Expand All @@ -40,6 +41,7 @@ public function __construct(
private readonly UserService $userService,
private readonly LoggerInterface $logger,
private readonly WayfResetLinkBuilder $wayfResetLinkBuilder,
private readonly WayfResetResultResolver $wayfResetResultResolver,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly string $wayfResetUrl,
private readonly CsrfTokenManagerInterface $csrfTokenManager,
Expand All @@ -52,8 +54,9 @@ public function __construct(
methods: ["GET"],
schemes: "https",
)]
public function overview(): Response
{
public function overview(
Request $request,
): Response {
$this->logger->info('Showing My Profile page');

$user = $this->userService->getUser();
Expand All @@ -62,6 +65,7 @@ public function overview(): Response
'@OpenConextProfile/MyProfile/overview.html.twig',
[
'user' => $user,
'wayfResetResult' => $this->wayfResetResultResolver->resolve($request),
],
);
}
Expand Down
46 changes: 46 additions & 0 deletions src/OpenConext/ProfileBundle/Service/WayfResetResultResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types = 1);

/**
* Copyright 2026 SURFnet B.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace OpenConext\ProfileBundle\Service;

use Symfony\Component\HttpFoundation\Request;

/**
* EngineBlock's /reset-remember-wayf endpoint redirects back to the My Profile page with
* a `wayfReset` query parameter to signal whether a remembered-choice cookie was actually
* removed or whether there was none to begin with. This resolver reads that parameter and
* limits it to the values Profile knows how to give feedback about, so an unrecognised or
* tampered-with value never triggers the feedback modal.
*/
final readonly class WayfResetResultResolver
{
public const string RESULT_REMOVED = 'removed';
public const string RESULT_NONE = 'none';

private const array KNOWN_RESULTS = [self::RESULT_REMOVED, self::RESULT_NONE];

public function resolve(
Request $request,
): ?string {
$result = $request->query->get('wayfReset');

return in_array($result, self::KNOWN_RESULTS, true) ? $result : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OpenConext\ProfileBundle\Service\SupportContactEmailService;
use OpenConext\ProfileBundle\Service\UserService;
use OpenConext\ProfileBundle\Service\WayfResetLinkBuilder;
use OpenConext\ProfileBundle\Service\WayfResetResultResolver;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -64,6 +65,7 @@ public function it_redirects_to_the_wayf_reset_url_when_the_csrf_token_is_valid(
$this->createUserService(),
$this->createMock(LoggerInterface::class),
new WayfResetLinkBuilder(),
new WayfResetResultResolver(),
$urlGenerator,
'https://engine.example.org/reset-remember-wayf',
$csrfTokenManager,
Expand Down Expand Up @@ -96,6 +98,7 @@ public function it_denies_access_when_the_csrf_token_is_invalid(): void
$this->createUserService(),
$this->createMock(LoggerInterface::class),
new WayfResetLinkBuilder(),
new WayfResetResultResolver(),
$urlGenerator,
'https://engine.example.org/reset-remember-wayf',
$csrfTokenManager,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types = 1);

/**
* Copyright 2026 SURFnet B.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace OpenConext\ProfileBundle\Tests\Service;

use OpenConext\ProfileBundle\Service\WayfResetResultResolver;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;

class WayfResetResultResolverTest extends TestCase
{
/**
* @test
* @group Service
*/
public function it_resolves_the_removed_result(): void
{
$resolver = new WayfResetResultResolver();

$result = $resolver->resolve(Request::create('/my-profile?wayfReset=removed'));

$this->assertSame(WayfResetResultResolver::RESULT_REMOVED, $result);
}

/**
* @test
* @group Service
*/
public function it_resolves_the_none_result(): void
{
$resolver = new WayfResetResultResolver();

$result = $resolver->resolve(Request::create('/my-profile?wayfReset=none'));

$this->assertSame(WayfResetResultResolver::RESULT_NONE, $result);
}

/**
* @test
* @group Service
*/
public function it_returns_null_when_the_query_parameter_is_missing(): void
{
$resolver = new WayfResetResultResolver();

$result = $resolver->resolve(Request::create('/my-profile'));

$this->assertNull($result);
}

/**
* @test
* @group Service
*/
public function it_returns_null_for_an_unrecognised_value(): void
{
$resolver = new WayfResetResultResolver();

$result = $resolver->resolve(Request::create('/my-profile?wayfReset=<script>alert(1)</script>'));

$this->assertNull($result);
}
}
30 changes: 30 additions & 0 deletions templates/MyProfile/overview.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,34 @@
</div>
</section>

{% if wayfResetResult is not empty %}
{% set resultModalContent %}
<p id="wayf-reset-result-description">
{% if wayfResetResult == 'removed' %}
{{ 'profile.my_profile.wayf_reset.result_removed_message'|trans }}
{% else %}
{{ 'profile.my_profile.wayf_reset.result_none_message'|trans }}
{% endif %}
</p>
{% endset %}

{# Explicitly clear buttonRowContent: `include` without `only` inherits the
surrounding scope, so without this the form set for the modal above would
leak into this single-button modal. #}
{% set buttonRowContent = '' %}
{% include "@OpenConextProfile/helpers/modal.html.twig" with {
buttonText: 'profile.my_profile.wayf_reset.link_text'|trans,
linkId: 'wayf_reset_result',
buttonClass: 'modalWindowButton visually-hidden',
describedBy: 'wayf-reset-result-description',
modalHeader: wayfResetResult == 'removed'
? 'profile.my_profile.wayf_reset.result_removed_title'|trans
: 'profile.my_profile.wayf_reset.result_none_title'|trans,
modalContent: resultModalContent,
cancelButtonText: 'general.ok'|trans,
autoOpen: true,
cleanParam: 'wayfReset',
} %}
{% endif %}

{% endblock %}
11 changes: 8 additions & 3 deletions templates/helpers/modal.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
{% if linkId is not defined %}
{% set linkId = linkIdPrefix ~ loop.index %}
{% endif %}
{% set headerId = 'dialog-header-' ~ loop.index %}
{# Derive headerId from linkId (always unique) rather than loop.index, so that
multiple modal instances rendered outside of a real iteration (e.g. two
independent modals on the same page) don't end up with duplicate ids. #}
{% set headerId = 'dialog-header-' ~ linkId %}

<a
class="button__secondary--white {{ buttonClass }}"
data-id="{{ linkId }}"
href="#{{ linkId }}"
role="button"
{% if buttonDescription is defined %} aria-describedBy="{{ buttonDescription }}"{% endif %}
{% if autoOpen is defined and autoOpen %} data-auto-open="true" tabindex="-1"{% endif %}
{% if cleanParam is defined %} data-clean-param="{{ cleanParam }}"{% endif %}
>
{{ buttonText }}
</a>
Expand All @@ -29,8 +34,8 @@
>{{ modalHeader }}</h4>
<div class="modalWindow__main">
{{ modalContent }}
<div class="modalWindow__buttonRow">
<a href="#ok" class="button button__secondary--white modalWindow__button modalWindow__cancel" data-to={{ linkId }}>{{ 'general.cancel'|trans }}</a>
<div class="modalWindow__buttonRow{% if buttonRowContent is not defined or buttonRowContent|trim is empty %} modalWindow__buttonRow--single{% endif %}">
<a href="#ok" class="button button__secondary--white modalWindow__button modalWindow__cancel" data-to={{ linkId }}>{{ cancelButtonText|default('general.cancel'|trans) }}</a>
{{ buttonRowContent }}
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions translations/messages.en.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
'cancel' => 'Cancel',
'confirm' => 'Confirm',
'home' => 'Home',
'ok' => 'OK',
'organisation_noun' => 'organisation',
'suite_name' => 'OpenConext',
],
Expand Down Expand Up @@ -192,6 +193,10 @@
'link_text' => 'Reset login method choices',
'modal_explanation' => 'This removes your saved login method choice for every application. The next time you log in, %suiteName% will ask you to choose a login method again.',
'modal_title' => 'Reset login method choices',
'result_none_message' => 'There was nothing to reset: you did not have any saved login method choices.',
'result_none_title' => 'Nothing to reset',
'result_removed_message' => 'Your saved login method choices have been removed. The next time you log in, %suiteName% will ask you to choose a login method again.',
'result_removed_title' => 'Login method choices reset',
'title' => 'Login method choices',
],
],
Expand Down
5 changes: 5 additions & 0 deletions translations/messages.nl.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'cancel' => 'Annuleren',
'confirm' => 'Bevestigen',
'home' => 'Naar de homepagina',
'ok' => 'OK',
'organisation_noun' => 'organisatie',
'suite_name' => 'OpenConext',
],
Expand Down Expand Up @@ -195,6 +196,10 @@
'link_text' => 'Inlogmethode-keuzes resetten',
'modal_explanation' => 'Hiermee verwijder je je opgeslagen inlogmethode-keuze voor elke applicatie. De volgende keer dat je inlogt, vraagt %suiteName% je opnieuw om een inlogmethode te kiezen.',
'modal_title' => 'Inlogmethode-keuzes resetten',
'result_none_message' => 'Er was niets om te resetten: je had geen opgeslagen inlogmethode-keuzes.',
'result_none_title' => 'Niets om te resetten',
'result_removed_message' => 'Je opgeslagen inlogmethode-keuzes zijn verwijderd. De volgende keer dat je inlogt, vraagt %suiteName% je opnieuw om een inlogmethode te kiezen.',
'result_removed_title' => 'Inlogmethode-keuzes gereset',
'title' => 'Inlogmethode-keuzes',
],
],
Expand Down
5 changes: 5 additions & 0 deletions translations/messages.pt.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
'cancel' => 'Cancel',
'confirm' => 'Confirm',
'home' => 'Home',
'ok' => 'OK',
'organisation_noun' => 'organização',
'suite_name' => 'OpenConext',
],
Expand Down Expand Up @@ -193,6 +194,10 @@
'link_text' => 'Repor escolhas de método de início de sessão',
'modal_explanation' => 'Isto remove a sua escolha guardada de método de início de sessão para todos os serviços. Da próxima vez que iniciar sessão, %suiteName% pedir-lhe-á novamente para escolher um método de início de sessão.',
'modal_title' => 'Repor escolhas de método de início de sessão',
'result_none_message' => 'Não havia nada para repor: não tinha nenhuma escolha de método de início de sessão guardada.',
'result_none_title' => 'Nada para repor',
'result_removed_message' => 'As suas escolhas guardadas de método de início de sessão foram removidas. Da próxima vez que iniciar sessão, %suiteName% pedir-lhe-á novamente para escolher um método de início de sessão.',
'result_removed_title' => 'Escolhas de método de início de sessão repostas',
'title' => 'Escolhas de método de início de sessão',
],
],
Expand Down
Loading