Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
96 / 96 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
ExecuteEmailReactionEndpoint | |
100.00% |
96 / 96 |
|
100.00% |
6 / 6 |
29 | |
100.00% |
1 / 1 |
handle | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
7 | |||
actionUnsubscribe | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
6 | |||
removeNotificationSubscription | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
actionResetPassword | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
actionVerifyEmail | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
6 | |||
actionDeleteNews | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace Olz\Api\Endpoints; |
4 | |
5 | use Olz\Api\OlzTypedEndpoint; |
6 | use Olz\Entity\News\NewsEntry; |
7 | use Olz\Entity\NotificationSubscription; |
8 | use Olz\Entity\Users\User; |
9 | |
10 | /** |
11 | * @extends OlzTypedEndpoint< |
12 | * array{ |
13 | * token: non-empty-string, |
14 | * }, |
15 | * array{ |
16 | * status: 'INVALID_TOKEN'|'OK', |
17 | * } |
18 | * > |
19 | */ |
20 | class ExecuteEmailReactionEndpoint extends OlzTypedEndpoint { |
21 | /** @var ?array<string, mixed> */ |
22 | protected ?array $reaction_data; |
23 | |
24 | protected function handle(mixed $input): mixed { |
25 | $token = $input['token']; |
26 | $this->reaction_data = $this->emailUtils()->decryptEmailReactionToken($token); |
27 | |
28 | if (!$this->reaction_data) { |
29 | $this->log()->error("Invalid email reaction token: {$token}", [$this->reaction_data]); |
30 | return ['status' => 'INVALID_TOKEN']; |
31 | } |
32 | |
33 | $action = $this->reaction_data['action'] ?? null; |
34 | switch ($action) { |
35 | case 'unsubscribe': |
36 | return $this->actionUnsubscribe(); |
37 | case 'reset_password': |
38 | return $this->actionResetPassword(); |
39 | case 'verify_email': |
40 | return $this->actionVerifyEmail(); |
41 | case 'delete_news': |
42 | return $this->actionDeleteNews(); |
43 | default: |
44 | $this->log()->error("Unknown email reaction action: {$action}.", [$this->reaction_data]); |
45 | return ['status' => 'INVALID_TOKEN']; |
46 | } |
47 | } |
48 | |
49 | protected function actionUnsubscribe(): mixed { |
50 | $user = intval($this->reaction_data['user'] ?? '0'); |
51 | if ($user <= 0) { |
52 | $this->log()->error("Invalid user {$user} to unsubscribe from email notifications.", [$this->reaction_data]); |
53 | return ['status' => 'INVALID_TOKEN']; |
54 | } |
55 | $notification_subscription_repo = $this->entityManager()->getRepository(NotificationSubscription::class); |
56 | if (isset($this->reaction_data['notification_type'])) { |
57 | $notification_type = $this->reaction_data['notification_type']; |
58 | $subscriptions = $notification_subscription_repo->findBy([ |
59 | 'delivery_type' => NotificationSubscription::DELIVERY_EMAIL, |
60 | 'notification_type' => $notification_type, |
61 | 'user' => $user, |
62 | ]); |
63 | foreach ($subscriptions as $subscription) { |
64 | $this->log()->notice("Removing email subscription: {$subscription}."); |
65 | $this->removeNotificationSubscription($subscription); |
66 | } |
67 | $this->entityManager()->flush(); |
68 | $this->log()->notice("Email subscriptions removed.", [$this->reaction_data]); |
69 | return ['status' => 'OK']; |
70 | } |
71 | if (isset($this->reaction_data['notification_type_all'])) { |
72 | $subscriptions = $notification_subscription_repo->findBy([ |
73 | 'delivery_type' => NotificationSubscription::DELIVERY_EMAIL, |
74 | 'user' => $user, |
75 | ]); |
76 | foreach ($subscriptions as $subscription) { |
77 | $this->log()->notice("Removing email subscription: {$subscription}.", [$this->reaction_data]); |
78 | $this->removeNotificationSubscription($subscription); |
79 | } |
80 | $this->entityManager()->flush(); |
81 | $this->log()->notice("Email subscriptions removed.", [$this->reaction_data]); |
82 | return ['status' => 'OK']; |
83 | } |
84 | $this->log()->error("Invalid email notification type to unsubscribe from.", [$this->reaction_data]); |
85 | return ['status' => 'INVALID_TOKEN']; |
86 | } |
87 | |
88 | protected function removeNotificationSubscription(NotificationSubscription $subscription): void { |
89 | // If it is an autogenerated reminder subscription, just mark it cancelled. |
90 | if ( |
91 | $subscription->getNotificationType() === NotificationSubscription::TYPE_EMAIL_CONFIG_REMINDER |
92 | || $subscription->getNotificationType() === NotificationSubscription::TYPE_ROLE_REMINDER |
93 | ) { |
94 | $args = json_decode($subscription->getNotificationTypeArgs() ?? '{}', true) ?? []; |
95 | $args['cancelled'] = true; |
96 | $subscription->setNotificationTypeArgs(json_encode($args) ?: '{}'); |
97 | } else { |
98 | $this->entityManager()->remove($subscription); |
99 | } |
100 | } |
101 | |
102 | protected function actionResetPassword(): mixed { |
103 | $user_id = intval($this->reaction_data['user'] ?? '0'); |
104 | $user_repo = $this->entityManager()->getRepository(User::class); |
105 | $user = $user_repo->findOneBy(['id' => $user_id]); |
106 | if (!$user) { |
107 | $this->log()->error("Invalid user {$user_id} to reset password.", [$this->reaction_data]); |
108 | return ['status' => 'INVALID_TOKEN']; |
109 | } |
110 | $new_password = $this->reaction_data['new_password'] ?? ''; |
111 | if (strlen($new_password) < 8) { |
112 | $this->log()->error("New password is too short.", [$this->reaction_data]); |
113 | return ['status' => 'INVALID_TOKEN']; |
114 | } |
115 | $user->setPasswordHash($this->authUtils()->hashPassword($new_password)); |
116 | $this->entityManager()->flush(); |
117 | return ['status' => 'OK']; |
118 | } |
119 | |
120 | protected function actionVerifyEmail(): mixed { |
121 | $user_id = intval($this->reaction_data['user'] ?? '0'); |
122 | $user_repo = $this->entityManager()->getRepository(User::class); |
123 | $user = $user_repo->findOneBy(['id' => $user_id]); |
124 | if (!$user) { |
125 | $this->log()->error("Invalid user {$user_id} to verify email.", [$this->reaction_data]); |
126 | return ['status' => 'INVALID_TOKEN']; |
127 | } |
128 | $verify_email = $this->reaction_data['email'] ?? ''; |
129 | $user_email = $user->getEmail(); |
130 | if ($verify_email !== $user_email) { |
131 | $this->log()->error("Trying to verify email ({$verify_email}) for user {$user_id} (email: {$user_email}).", [$this->reaction_data]); |
132 | return ['status' => 'INVALID_TOKEN']; |
133 | } |
134 | $verify_token = $this->reaction_data['token'] ?? null; |
135 | $user_token = $user->getEmailVerificationToken(); |
136 | if (!$verify_token || !$user_token || $verify_token !== $user_token) { |
137 | $this->log()->error("Invalid email verification token {$verify_token} for user {$user_id} (token: {$user_token}).", [$this->reaction_data]); |
138 | return ['status' => 'INVALID_TOKEN']; |
139 | } |
140 | $user->setEmailIsVerified(true); |
141 | $user->setEmailVerificationToken(null); |
142 | $user->addPermission('verified_email'); |
143 | $this->entityManager()->flush(); |
144 | return ['status' => 'OK']; |
145 | } |
146 | |
147 | protected function actionDeleteNews(): mixed { |
148 | $news_id = $this->reaction_data['news_id'] ?? null; |
149 | $news_repo = $this->entityManager()->getRepository(NewsEntry::class); |
150 | $news_entry = $news_repo->findOneBy(['id' => $news_id]); |
151 | if (!$news_id || !$news_entry) { |
152 | $this->log()->error("Trying to delete inexistent news entry: {$news_id}.", [$this->reaction_data]); |
153 | return ['status' => 'INVALID_TOKEN']; |
154 | } |
155 | $this->entityUtils()->updateOlzEntity($news_entry, ['onOff' => false]); |
156 | $this->entityManager()->flush(); |
157 | return ['status' => 'OK']; |
158 | } |
159 | } |