Passed
Pull Request — master (#165)
by Garion
02:03
created

SecurityAdminExtension::sendResetEmail()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 16
nc 3
nop 1
dl 0
loc 23
rs 9.7333
c 0
b 0
f 0
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\Control\Email\Email;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Core\Extension;
11
use SilverStripe\Admin\SecurityAdmin;
12
use SilverStripe\MFA\Extension\MemberExtension;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, SilverStripe\MFA\Extensi...ntReset\MemberExtension. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
13
use SilverStripe\MFA\JSONResponse;
14
use SilverStripe\ORM\ValidationException;
15
use SilverStripe\Security\Member;
16
use SilverStripe\Security\PasswordEncryptor_NotFoundException;
17
use SilverStripe\Security\Permission;
18
use SilverStripe\Security\Security;
19
use SilverStripe\Security\SecurityToken;
20
21
/**
22
 * This extension is applied to SecurityAdmin to provide an additional endpoint
23
 * for sending account reset requests.
24
 *
25
 * @package SilverStripe\MFA\Extension
26
 * @property SecurityAdmin $owner
27
 */
28
class SecurityAdminExtension extends Extension
29
{
30
    use JSONResponse;
31
32
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
33
        'reset',
34
    ];
35
36
    /**
37
     * @var string[]
38
     */
39
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
40
        'Logger' => '%$' . LoggerInterface::class . '.account_reset',
41
    ];
42
43
    /**
44
     * @var LoggerInterface
45
     */
46
    protected $logger;
47
48
    public function reset(HTTPRequest $request): HTTPResponse
49
    {
50
        if (!$request->isPOST() || !$request->param('ID')) {
51
            return $this->jsonResponse(
52
                [
53
                    'error' => _t(__CLASS__ . '.BAD_REQUEST', 'Invalid request')
54
                ],
55
                400
56
            );
57
        }
58
59
        $body = json_decode($request->getBody() ?? '', true);
60
61
        if (!SecurityToken::inst()->check($body['csrf_token'] ?? null)) {
62
            return $this->jsonResponse(
63
                [
64
                    'error' => _t(__CLASS__ . '.INVALID_CSRF_TOKEN', 'Invalid or missing CSRF token')
65
                ],
66
                400
67
            );
68
        }
69
70
        if (!Permission::check(MemberExtension::MFA_ADMINISTER_REGISTERED_METHODS)) {
71
            return $this->jsonResponse(
72
                [
73
                    'error' => _t(
74
                        __CLASS__ . '.INSUFFICIENT_PERMISSIONS',
75
                        'Insufficient permissions to reset user'
76
                    )
77
                ],
78
                403
79
            );
80
        }
81
82
        /** @var Member $memberToReset */
83
        $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

83
        $memberToReset = Member::get()->byID(/** @scrutinizer ignore-type */ $request->param('ID'));
Loading history...
84
85
        if ($memberToReset === null) {
86
            return $this->jsonResponse(
87
                [
88
                    'error' => _t(
89
                        __CLASS__ . '.INVALID_MEMBER',
90
                        'Requested member for reset not found'
91
                    )
92
                ],
93
                403
94
            );
95
        }
96
97
        $sent = $this->sendResetEmail($memberToReset);
98
99
        if (!$sent) {
100
            return $this->jsonResponse(
101
                [
102
                    'error' => _t(
103
                        __CLASS__ . '.EMAIL_NOT_SENT',
104
                        'Email sending failed'
105
                    )
106
                ],
107
                500
108
            );
109
        }
110
111
        return $this->jsonResponse(['success' => true], 200);
112
    }
113
114
    /**
115
     * @param Member&MemberResetExtension $member
116
     * @return bool
117
     * @throws ValidationException
118
     * @throws PasswordEncryptor_NotFoundException
119
     */
120
    protected function sendResetEmail($member)
121
    {
122
        // Generate / store / obtain reset token
123
        $token = $member->generateAccountResetTokenAndStoreHash();
124
125
        // Create email and fire
126
        try {
127
            $email = Email::create()
128
                ->setHTMLTemplate('SilverStripe\\MFA\\Email\\AccountReset')
129
                ->setData($member)
130
                ->setSubject(_t(
131
                    __CLASS__ . '.ACCOUNT_RESET_EMAIL_SUBJECT',
132
                    'Reset your account'
133
                ))
134
                ->addData('AccountResetLink', $this->getAccountResetLink($member, $token))
135
                ->addData('Member', $member)
136
                ->setFrom(Email::config()->get('admin_email'))
137
                ->setTo($member->Email);
138
139
            return $email->send();
140
        } catch (Exception $e) {
141
            $this->logger->info('WARNING: Account Reset Email failed to send: ' . $e->getMessage());
142
            return false;
143
        }
144
    }
145
146
    /**
147
     * Generates a link to the Account Reset Handler endpoint to be sent to a Member.
148
     *
149
     * @param Member $member
150
     * @param string $token
151
     * @return string
152
     */
153
    protected function getAccountResetLink(Member $member, string $token): string
154
    {
155
        return Controller::join_links(
0 ignored issues
show
Bug introduced by
The type SilverStripe\MFA\Extension\AccountReset\Controller was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
156
            Security::singleton()->Link('resetaccount'),
157
            "?m={$member->ID}&t={$token}"
158
        );
159
    }
160
}
161