Skip to content

Commit 29b8785

Browse files
Make /deleteprofile accessible without redirect from Play store compliance.
1 parent 49ef4a5 commit 29b8785

File tree

11 files changed

+383
-8
lines changed

11 files changed

+383
-8
lines changed

config/packages/security.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ security:
9999
- { path: ^/volunteer, roles: PUBLIC_ACCESS }
100100
- { path: ^/communitynews, roles: PUBLIC_ACCESS }
101101
- { path: ^/members/avatar/, roles: PUBLIC_ACCESS }
102+
- { path: ^/deleteprofile, roles: PUBLIC_ACCESS }
102103
- { path: ^/admin, roles: ROLE_ADMIN }
103104
- { path: ^/api, roles: PUBLIC_ACCESS }
104105
- { path: ^/, roles: ROLE_USER }

config/services.yaml

+11-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ parameters:
1919
forum_notification_batch_size: '%env(int:forum_notification_batch_size)%'
2020
locales: "%env(csv:LOCALES)%"
2121
document_locales: "%env(DOCUMENT_LOCALES)%"
22-
app.username.regexp: !php/const App\Entity\Member::USERNAME_REGEXP
22+
username.pattern: "/[a-z](?!.*[-_.][-_.])[a-z0-9-._]{2,18}[a-z0-9]/i"
2323
manticore.host: '%env(MANTICORE_HOST)%'
2424
manticore.port: '%env(int:MANTICORE_PORT)%'
2525

@@ -36,6 +36,7 @@ services:
3636
$manticoreHost: '%manticore.host%'
3737
$manticorePort: '%manticore.port%'
3838
$locales: '%locales%'
39+
$formLoginAuthenticator: '@security.authenticator.form_login.main'
3940

4041
# makes classes in src/ available to be used as services
4142
# this creates a service per class whose id is the fully-qualified class name
@@ -254,7 +255,6 @@ services:
254255
app.user_locale_listener:
255256
public: true
256257
class: App\EventListener\UserLocaleListener
257-
arguments: [ '@session', '@doctrine.orm.entity_manager' ]
258258
tags:
259259
- { name: kernel.event_subscriber }
260260

@@ -475,3 +475,12 @@ services:
475475
app.mockup_provider.comments:
476476
class: App\Model\MockupProvider\CommentMockups
477477
tags: [ 'app.mockup_provider' ]
478+
479+
app.mockup_provider.profile:
480+
class: App\Model\MockupProvider\ProfileMockups
481+
tags: [ 'app.mockup_provider' ]
482+
483+
# app.signup.form:
484+
# class: App\Form\SignupFormType
485+
# arguments:
486+
# $usernamePattern: '%username.pattern%'

src/Controller/ProfileController.php

+123-5
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77
use App\Entity\NewLocation;
88
use App\Entity\Preference;
99
use App\Entity\ProfileVisit;
10+
use App\Form\DeleteProfileFormType;
1011
use App\Form\ProfileStatusFormType;
1112
use App\Form\SearchLocationType;
1213
use App\Form\SetLocationType;
14+
use App\Model\ProfileModel;
1315
use App\Repository\ProfileVisitRepository;
1416
use App\Utilities\ChangeProfilePictureGlobals;
1517
use App\Utilities\ProfileSubmenu;
1618
use Doctrine\ORM\EntityManagerInterface;
1719
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
1820
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
21+
use Symfony\Component\Form\FormError;
1922
use Symfony\Component\HttpFoundation\RedirectResponse;
2023
use Symfony\Component\HttpFoundation\Request;
2124
use Symfony\Component\HttpFoundation\Response;
25+
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
2226
use Symfony\Component\Routing\Annotation\Route;
2327
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
2428
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -27,11 +31,16 @@ class ProfileController extends AbstractController
2731
{
2832
private ProfileSubmenu $profileSubmenu;
2933
private ChangeProfilePictureGlobals $globals;
34+
private EntityManagerInterface $entityManager;
3035

31-
public function __construct(ChangeProfilePictureGlobals $globals, ProfileSubmenu $profileSubmenu)
32-
{
36+
public function __construct(
37+
ChangeProfilePictureGlobals $globals,
38+
ProfileSubmenu $profileSubmenu,
39+
EntityManagerInterface $entityManager
40+
) {
3341
$this->globals = $globals;
3442
$this->profileSubmenu = $profileSubmenu;
43+
$this->entityManager = $entityManager;
3544
}
3645

3746
/**
@@ -90,7 +99,6 @@ public function setMemberStatus(Request $request, EntityManagerInterface $entity
9099
*/
91100
public function showMyVisitors(
92101
Member $member,
93-
ProfileSubmenu $profileSubmenu,
94102
EntityManagerInterface $entityManager,
95103
int $page = 1
96104
): Response {
@@ -119,7 +127,7 @@ public function showMyVisitors(
119127
'member' => $member,
120128
'visits' => $visits,
121129
'globals_js_json' => $this->globals->getGlobalsJsAsJson($member, $loggedInMember),
122-
'submenu' => $profileSubmenu->getSubmenu($member, $loggedInMember, ['active' => 'visitors']),
130+
'submenu' => $this->profileSubmenu->getSubmenu($member, $loggedInMember, ['active' => 'visitors']),
123131
]);
124132
}
125133

@@ -137,7 +145,6 @@ public function redirectToSetLocation(): RedirectResponse
137145
public function setLocation(
138146
Request $request,
139147
Member $member,
140-
ProfileSubmenu $profileSubmenu,
141148
EntityManagerInterface $entityManager
142149
): Response {
143150
/** @var Member $loggedInMember */
@@ -188,6 +195,90 @@ public function setLocation(
188195
]);
189196
}
190197

198+
/**
199+
* @Route("/deleteprofile", name="profile_delete_redirect")
200+
*/
201+
public function deleteProfileNotLoggedIn(
202+
Request $request,
203+
ProfileModel $profileModel,
204+
TranslatorInterface $translator,
205+
PasswordHasherFactoryInterface $passwordHasherFactory
206+
): Response {
207+
/** @var Member $member */
208+
$member = $this->getUser();
209+
210+
if (null !== $member) {
211+
return $this->redirectToRoute('profile_delete', ['username' => $member->getUsername()]);
212+
}
213+
214+
$deleteProfileForm = $this->createForm(DeleteProfileFormType::class, null, [
215+
'loggedIn' => false,
216+
]);
217+
$deleteProfileForm->handleRequest($request);
218+
219+
if ($deleteProfileForm->isSubmitted() && $deleteProfileForm->isValid()) {
220+
$data = $deleteProfileForm->getData();
221+
$memberRepository = $this->entityManager->getRepository(Member::class);
222+
$member = $memberRepository->find($data['username']);
223+
224+
$verified = false;
225+
if (null === $member) {
226+
$deleteProfileForm->addError(new FormError($translator->trans('profile.delete.credentials')));
227+
} else {
228+
$passwordHasher = $passwordHasherFactory->getPasswordHasher($member);
229+
$verified = $passwordHasher->verify($member->getPassword(), $data['password']);
230+
231+
if (!$verified) {
232+
$deleteProfileForm->addError(new FormError($translator->trans('profile.delete.credentials')));
233+
}
234+
}
235+
236+
$success = false;
237+
if ($verified) {
238+
$success = $profileModel->retireProfile($member, $data);
239+
}
240+
241+
if ($success) {
242+
return $this->redirectToRoute('/logout');
243+
}
244+
}
245+
246+
return $this->render('profile/delete.not.logged.in.html.twig', [
247+
'form' => $deleteProfileForm->createView()
248+
]);
249+
}
250+
251+
/**
252+
* @Route("/members/{username}/delete", name="profile_delete")
253+
*/
254+
public function deleteProfile(Request $request, Member $member, ProfileModel $profileModel): Response
255+
{
256+
$loggedInMember = $this->getUser();
257+
if ($member !== $loggedInMember) {
258+
return $this->redirectToRoute('members_profile', ['username' => $member->getUsername()]);
259+
}
260+
261+
$deleteProfileForm = $this->createForm(DeleteProfileFormType::class, null, [
262+
'loggedIn' => true,
263+
]);
264+
$deleteProfileForm->handleRequest($request);
265+
266+
if ($deleteProfileForm->isSubmitted() && $deleteProfileForm->isValid()) {
267+
$success = $profileModel->retireProfile($member, $deleteProfileForm->getData());
268+
269+
if ($success) {
270+
return $this->redirectToRoute('security_logout');
271+
}
272+
}
273+
274+
return $this->render('profile/delete.html.twig', [
275+
'form' => $deleteProfileForm->createView(),
276+
'member' => $member,
277+
'globals_js_json' => $this->globals->getGlobalsJsAsJson($member, $member),
278+
'submenu' => $this->profileSubmenu->getSubmenu($member, $member, ['active' => 'profile']),
279+
]);
280+
}
281+
191282
private function renderProfile(bool $ownProfile, Member $member, Member $loggedInMember): Response
192283
{
193284
return $this->render('profile/show.html.twig', [
@@ -197,4 +288,31 @@ private function renderProfile(bool $ownProfile, Member $member, Member $loggedI
197288
'submenu' => $this->profileSubmenu->getSubmenu($member, $loggedInMember),
198289
]);
199290
}
291+
292+
private function deleteProfileProcess(Request $request, bool $loggedIn): Response
293+
{
294+
$deleteProfileForm = $this->createForm(DeleteProfileFormType::class, null, [
295+
'loggedIn' => $loggedIn,
296+
]);
297+
$deleteProfileForm->handleRequest($request);
298+
299+
if ($deleteProfileForm->isSubmitted() && $deleteProfileForm->isValid()) {
300+
$data = $deleteProfileForm->getData();
301+
if (false === $loggedIn) {
302+
// Check credentials
303+
}
304+
305+
// handle delete profile form.
306+
307+
return $this->redirectToRoute('logout');
308+
}
309+
310+
return $this->render('profile/delete.html.twig', [
311+
'form' => $deleteProfileForm->createView(),
312+
'member' => $member,
313+
'globals_js_json' => $this->globals->getGlobalsJsAsJson($member, $member),
314+
'submenu' => $profileSubmenu->getSubmenu($member, $member, ['active' => 'profile']),
315+
]);
316+
317+
}
200318
}

src/Form/DeleteProfileFormType.php

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace App\Form;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
7+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
8+
use Symfony\Component\Form\Extension\Core\Type\TextType;
9+
use Symfony\Component\Form\FormBuilderInterface;
10+
use Symfony\Component\OptionsResolver\OptionsResolver;
11+
use Symfony\Component\Validator\Constraints\NotBlank;
12+
use Symfony\Component\Validator\Constraints\NotNull;
13+
14+
class DeleteProfileFormType extends AbstractType
15+
{
16+
public function buildForm(FormBuilderInterface $builder, array $options)
17+
{
18+
$builder
19+
->add('feedback', CkEditorType::class, [
20+
'label' => 'profile.delete.feedback',
21+
'required' => false,
22+
])
23+
->add('data_retention', CheckboxType::class, [
24+
'label' => 'profile.delete.cleanup',
25+
'required' => false,
26+
])
27+
;
28+
if (false === $options['loggedIn']) {
29+
$builder
30+
->add('username', TextType::class, [
31+
'required' => false,
32+
'constraints' => [
33+
new NotNull(),
34+
new NotBlank(),
35+
],
36+
])
37+
->add('password', PasswordType::class, [
38+
'required' => false,
39+
'constraints' => [
40+
new NotNull(),
41+
new NotBlank(),
42+
],
43+
])
44+
;
45+
}
46+
}
47+
48+
public function configureOptions(OptionsResolver $resolver)
49+
{
50+
$resolver
51+
->setDefaults([
52+
'loggedIn' => false,
53+
])
54+
->addAllowedTypes('loggedIn', 'bool');
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace App\Model\MockupProvider;
4+
5+
use App\Entity\Member;
6+
use App\Form\DeleteProfileFormType;
7+
use App\Form\InvitationType;
8+
use Symfony\Component\Form\FormError;
9+
use Symfony\Component\Form\FormFactoryInterface;
10+
use Symfony\Contracts\Translation\TranslatorInterface;
11+
12+
class ProfileMockups implements MockupProviderInterface
13+
{
14+
private const MOCKUPS = [
15+
'delete profile' => [
16+
'type' => 'template',
17+
'template' => 'profile/delete.not.logged.in.html.twig',
18+
'description' => 'The delete profile page when not logged in',
19+
],
20+
'delete profile (wrong credentials)' => [
21+
'type' => 'template',
22+
'template' => 'profile/delete.not.logged.in.html.twig',
23+
'description' => 'The delete profile page when not logged in',
24+
],
25+
];
26+
private FormFactoryInterface $formFactory;
27+
private TranslatorInterface $translator;
28+
29+
public function __construct(FormFactoryInterface $formFactory, TranslatorInterface $translator)
30+
{
31+
$this->formFactory = $formFactory;
32+
$this->translator = $translator;
33+
}
34+
35+
public function getFeature(): string
36+
{
37+
return 'delete_profile';
38+
}
39+
40+
public function getMockups(): array
41+
{
42+
return self::MOCKUPS;
43+
}
44+
45+
public function getMockupVariables(array $parameters): array
46+
{
47+
switch ($parameters['name']) {
48+
case 'delete profile':
49+
return $this->getVariablesForDeleteProfile($parameters);
50+
case 'delete profile (wrong credentials)':
51+
return $this->getVariablesForDeleteProfileCredentialsError($parameters);
52+
default:
53+
return [];
54+
}
55+
}
56+
57+
public function getMockupParameter(?string $locale = null, ?string $feature = null): array
58+
{
59+
return [];
60+
}
61+
62+
private function getVariablesForDeleteProfile(array $parameters): array
63+
{
64+
$form = $this->formFactory->create(DeleteProfileFormType::class);
65+
66+
return [
67+
'form' => $form->createView(),
68+
];
69+
}
70+
71+
private function getVariablesForDeleteProfileCredentialsError(array $parameters): array
72+
{
73+
$form = $this->formFactory->create(DeleteProfileFormType::class);
74+
$form->addError(new FormError($this->translator->trans('profile.delete.credentials')));
75+
76+
return [
77+
'form' => $form->createView(),
78+
];
79+
}
80+
}

0 commit comments

Comments
 (0)