1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace SilverStripe\MFA\Extension\AccountReset; |
||||
6 | |||||
7 | use Exception; |
||||
8 | use Psr\Log\LoggerInterface; |
||||
9 | use SilverStripe\Admin\SecurityAdmin; |
||||
10 | use SilverStripe\Control\Controller; |
||||
11 | use SilverStripe\Control\Email\Email; |
||||
12 | use SilverStripe\Control\HTTPRequest; |
||||
13 | use SilverStripe\Control\HTTPResponse; |
||||
14 | use SilverStripe\Core\Extension; |
||||
15 | use SilverStripe\MFA\Extension\MemberExtension as BaseMFAMemberExtension; |
||||
16 | use SilverStripe\MFA\JSONResponse; |
||||
17 | use SilverStripe\Security\Member; |
||||
18 | use SilverStripe\Security\Permission; |
||||
19 | use SilverStripe\Security\Security; |
||||
20 | use SilverStripe\Security\SecurityToken; |
||||
21 | |||||
22 | /** |
||||
23 | * This extension is applied to SecurityAdmin to provide an additional endpoint |
||||
24 | * for sending account reset requests. |
||||
25 | * |
||||
26 | * @package SilverStripe\MFA\Extension |
||||
27 | * @property SecurityAdmin $owner |
||||
28 | */ |
||||
29 | class SecurityAdminExtension extends Extension |
||||
30 | { |
||||
31 | use JSONResponse; |
||||
32 | |||||
33 | private static $allowed_actions = [ |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
34 | 'reset', |
||||
35 | ]; |
||||
36 | |||||
37 | /** |
||||
38 | * @var string[] |
||||
39 | */ |
||||
40 | private static $dependencies = [ |
||||
0 ignored issues
–
show
|
|||||
41 | 'Logger' => '%$' . LoggerInterface::class . '.account_reset', |
||||
42 | ]; |
||||
43 | |||||
44 | /** |
||||
45 | * @var LoggerInterface |
||||
46 | */ |
||||
47 | protected $logger; |
||||
48 | |||||
49 | public function reset(HTTPRequest $request): HTTPResponse |
||||
50 | { |
||||
51 | if (!$request->isPOST() || !$request->param('ID')) { |
||||
52 | return $this->jsonResponse( |
||||
53 | [ |
||||
54 | 'error' => _t(__CLASS__ . '.BAD_REQUEST', 'Invalid request') |
||||
55 | ], |
||||
56 | 400 |
||||
57 | ); |
||||
58 | } |
||||
59 | |||||
60 | $body = json_decode($request->getBody() ?? '', true); |
||||
61 | |||||
62 | if (!SecurityToken::inst()->check($body['csrf_token'] ?? null)) { |
||||
63 | return $this->jsonResponse( |
||||
64 | [ |
||||
65 | 'error' => _t(__CLASS__ . '.INVALID_CSRF_TOKEN', 'Invalid or missing CSRF token') |
||||
66 | ], |
||||
67 | 400 |
||||
68 | ); |
||||
69 | } |
||||
70 | |||||
71 | if (!Permission::check(BaseMFAMemberExtension::MFA_ADMINISTER_REGISTERED_METHODS)) { |
||||
72 | return $this->jsonResponse( |
||||
73 | [ |
||||
74 | 'error' => _t( |
||||
75 | __CLASS__ . '.INSUFFICIENT_PERMISSIONS', |
||||
76 | 'Insufficient permissions to reset user' |
||||
77 | ) |
||||
78 | ], |
||||
79 | 403 |
||||
80 | ); |
||||
81 | } |
||||
82 | |||||
83 | /** @var Member $memberToReset */ |
||||
84 | $memberToReset = Member::get()->byID($request->param('ID')); |
||||
0 ignored issues
–
show
$request->param('ID') of type string is incompatible with the type integer expected by parameter $id of SilverStripe\ORM\DataList::byID() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
85 | |||||
86 | if ($memberToReset === null) { |
||||
87 | return $this->jsonResponse( |
||||
88 | [ |
||||
89 | 'error' => _t( |
||||
90 | __CLASS__ . '.INVALID_MEMBER', |
||||
91 | 'Requested member for reset not found' |
||||
92 | ) |
||||
93 | ], |
||||
94 | 403 |
||||
95 | ); |
||||
96 | } |
||||
97 | |||||
98 | $sent = $this->sendResetEmail($memberToReset); |
||||
99 | |||||
100 | if (!$sent) { |
||||
101 | return $this->jsonResponse( |
||||
102 | [ |
||||
103 | 'error' => _t( |
||||
104 | __CLASS__ . '.EMAIL_NOT_SENT', |
||||
105 | 'Email sending failed' |
||||
106 | ) |
||||
107 | ], |
||||
108 | 500 |
||||
109 | ); |
||||
110 | } |
||||
111 | |||||
112 | return $this->jsonResponse(['success' => true], 200); |
||||
113 | } |
||||
114 | |||||
115 | /** |
||||
116 | * Prepares and attempts to send the Account Reset request email. |
||||
117 | * |
||||
118 | * @param Member&MemberExtension $member |
||||
119 | * @return bool |
||||
120 | */ |
||||
121 | protected function sendResetEmail($member) |
||||
122 | { |
||||
123 | // Generate / store / obtain reset token |
||||
124 | $token = $member->generateAccountResetTokenAndStoreHash(); |
||||
125 | |||||
126 | // Create email and fire |
||||
127 | try { |
||||
128 | $email = Email::create() |
||||
129 | ->setHTMLTemplate('SilverStripe\\MFA\\Email\\AccountReset') |
||||
130 | ->setData($member) |
||||
131 | ->setSubject(_t( |
||||
132 | __CLASS__ . '.ACCOUNT_RESET_EMAIL_SUBJECT', |
||||
133 | 'Reset your account' |
||||
134 | )) |
||||
135 | ->addData('AccountResetLink', $this->getAccountResetLink($member, $token)) |
||||
136 | ->addData('Member', $member) |
||||
137 | ->setFrom(Email::config()->get('admin_email')) |
||||
138 | ->setTo($member->Email); |
||||
139 | |||||
140 | return $email->send(); |
||||
141 | } catch (Exception $e) { |
||||
142 | $this->logger->info('WARNING: Account Reset Email failed to send: ' . $e->getMessage()); |
||||
143 | return false; |
||||
144 | } |
||||
145 | } |
||||
146 | |||||
147 | /** |
||||
148 | * Generates a link to the Account Reset Handler endpoint to be sent to a Member. |
||||
149 | * |
||||
150 | * @param Member $member |
||||
151 | * @param string $token |
||||
152 | * @return string |
||||
153 | */ |
||||
154 | public function getAccountResetLink(Member $member, string $token): string |
||||
155 | { |
||||
156 | return Controller::join_links( |
||||
157 | Security::singleton()->Link('resetaccount'), |
||||
158 | "?m={$member->ID}&t={$token}" |
||||
159 | ); |
||||
160 | } |
||||
161 | |||||
162 | /** |
||||
163 | * @param LoggerInterface|null $logger |
||||
164 | * @return SecurityAdmin |
||||
165 | */ |
||||
166 | public function setLogger(?LoggerInterface $logger): ?SecurityAdmin |
||||
167 | { |
||||
168 | $this->logger = $logger; |
||||
169 | return $this->owner; |
||||
170 | } |
||||
171 | } |
||||
172 |