Passed
Push — master ( 908be6...0b33b7 )
by Robbie
04:03 queued 10s
created

SecurityAdminExtension::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\Extension\AccountReset;
4
5
use Exception;
6
use Psr\Log\LoggerInterface;
7
use SilverStripe\Admin\SecurityAdmin;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\Email\Email;
10
use SilverStripe\Control\HTTPRequest;
11
use SilverStripe\Control\HTTPResponse;
12
use SilverStripe\Core\Extension;
13
use SilverStripe\MFA\Extension\MemberExtension as BaseMFAMemberExtension;
14
use SilverStripe\MFA\JSONResponse;
15
use SilverStripe\Security\Member;
16
use SilverStripe\Security\Permission;
17
use SilverStripe\Security\Security;
18
use SilverStripe\Security\SecurityToken;
19
20
/**
21
 * This extension is applied to SecurityAdmin to provide an additional endpoint
22
 * for sending account reset requests.
23
 *
24
 * @package SilverStripe\MFA\Extension
25
 * @property SecurityAdmin $owner
26
 */
27
class SecurityAdminExtension extends Extension
28
{
29
    use JSONResponse;
30
31
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
32
        'reset',
33
    ];
34
35
    /**
36
     * @var string[]
37
     */
38
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
39
        'Logger' => '%$' . LoggerInterface::class . '.account_reset',
40
    ];
41
42
    /**
43
     * @var LoggerInterface
44
     */
45
    protected $logger;
46
47
    public function reset(HTTPRequest $request): HTTPResponse
48
    {
49
        if (!$request->isPOST() || !$request->param('ID')) {
50
            return $this->jsonResponse(
51
                [
52
                    'error' => _t(__CLASS__ . '.BAD_REQUEST', 'Invalid request')
53
                ],
54
                400
55
            );
56
        }
57
58
        $body = json_decode($request->getBody() ?? '', true);
59
60
        if (!SecurityToken::inst()->check($body['csrf_token'] ?? null)) {
61
            return $this->jsonResponse(
62
                [
63
                    'error' => _t(__CLASS__ . '.INVALID_CSRF_TOKEN', 'Invalid or missing CSRF token')
64
                ],
65
                400
66
            );
67
        }
68
69
        if (!Permission::check(BaseMFAMemberExtension::MFA_ADMINISTER_REGISTERED_METHODS)) {
70
            return $this->jsonResponse(
71
                [
72
                    'error' => _t(
73
                        __CLASS__ . '.INSUFFICIENT_PERMISSIONS',
74
                        'Insufficient permissions to reset user'
75
                    )
76
                ],
77
                403
78
            );
79
        }
80
81
        /** @var Member $memberToReset */
82
        $memberToReset = Member::get()->byID($request->param('ID'));
0 ignored issues
show
Bug introduced by
$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 ignore-type  annotation

82
        $memberToReset = Member::get()->byID(/** @scrutinizer ignore-type */ $request->param('ID'));
Loading history...
83
84
        if ($memberToReset === null) {
85
            return $this->jsonResponse(
86
                [
87
                    'error' => _t(
88
                        __CLASS__ . '.INVALID_MEMBER',
89
                        'Requested member for reset not found'
90
                    )
91
                ],
92
                403
93
            );
94
        }
95
96
        $sent = $this->sendResetEmail($memberToReset);
97
98
        if (!$sent) {
99
            return $this->jsonResponse(
100
                [
101
                    'error' => _t(
102
                        __CLASS__ . '.EMAIL_NOT_SENT',
103
                        'Email sending failed'
104
                    )
105
                ],
106
                500
107
            );
108
        }
109
110
        return $this->jsonResponse(['success' => true], 200);
111
    }
112
113
    /**
114
     * Prepares and attempts to send the Account Reset request email.
115
     *
116
     * @param Member&MemberExtension $member
117
     * @return bool
118
     */
119
    protected function sendResetEmail($member)
120
    {
121
        // Generate / store / obtain reset token
122
        $token = $member->generateAccountResetTokenAndStoreHash();
123
124
        // Create email and fire
125
        try {
126
            $email = Email::create()
127
                ->setHTMLTemplate('SilverStripe\\MFA\\Email\\AccountReset')
128
                ->setData($member)
129
                ->setSubject(_t(
130
                    __CLASS__ . '.ACCOUNT_RESET_EMAIL_SUBJECT',
131
                    'Reset your account'
132
                ))
133
                ->addData('AccountResetLink', $this->getAccountResetLink($member, $token))
134
                ->addData('Member', $member)
135
                ->setFrom(Email::config()->get('admin_email'))
136
                ->setTo($member->Email);
137
138
            return $email->send();
139
        } catch (Exception $e) {
140
            $this->logger->info('WARNING: Account Reset Email failed to send: ' . $e->getMessage());
141
            return false;
142
        }
143
    }
144
145
    /**
146
     * Generates a link to the Account Reset Handler endpoint to be sent to a Member.
147
     *
148
     * @param Member $member
149
     * @param string $token
150
     * @return string
151
     */
152
    public function getAccountResetLink(Member $member, string $token): string
153
    {
154
        return Controller::join_links(
155
            Security::singleton()->Link('resetaccount'),
156
            "?m={$member->ID}&t={$token}"
157
        );
158
    }
159
160
    /**
161
     * @param LoggerInterface|null $logger
162
     * @return SecurityAdmin
163
     */
164
    public function setLogger(?LoggerInterface $logger): ?SecurityAdmin
165
    {
166
        $this->logger = $logger;
167
        return $this->owner;
168
    }
169
}
170