Passed
Pull Request — 4 (#7399)
by Damian
09:42
created

LostPasswordHandler::Link()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\MemberAuthenticator;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Email\Email;
7
use SilverStripe\Control\HTTPResponse;
8
use SilverStripe\Control\RequestHandler;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Forms\Form;
11
use SilverStripe\ORM\FieldType\DBField;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\Security;
14
15
/**
16
 * Handle login requests from MemberLoginForm
17
 */
18
class LostPasswordHandler extends RequestHandler
19
{
20
    /**
21
     * Authentication class to use
22
     * @var string
23
     */
24
    protected $authenticatorClass = MemberAuthenticator::class;
25
26
    /**
27
     * @var array
28
     */
29
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
30
        'passwordsent/$EmailAddress' => 'passwordsent',
31
        ''                           => 'lostpassword',
32
    ];
33
34
    /**
35
     * Since the logout and dologin actions may be conditionally removed, it's necessary to ensure these
36
     * remain valid actions regardless of the member login state.
37
     *
38
     * @var array
39
     * @config
40
     */
41
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
42
        'lostpassword',
43
        'LostPasswordForm',
44
        'passwordsent',
45
    ];
46
47
    /**
48
     * Link to this handler
49
     *
50
     * @var string
51
     */
52
    protected $link = null;
53
54
    /**
55
     * @param string $link The URL to recreate this request handler
56
     */
57
    public function __construct($link)
58
    {
59
        $this->link = $link;
60
        parent::__construct();
61
    }
62
63
    /**
64
     * Return a link to this request handler.
65
     * The link returned is supplied in the constructor
66
     *
67
     * @param string|null $action
68
     * @return string
69
     */
70
    public function Link($action = null)
71
    {
72
        $link = Controller::join_links($this->link, $action);
73
        $this->extend('updateLink', $link, $action);
74
        return $link;
75
    }
76
77
    /**
78
     * URL handler for the initial lost-password screen
79
     *
80
     * @return array
81
     */
82
    public function lostpassword()
83
    {
84
85
        $message = _t(
86
            'SilverStripe\\Security\\Security.NOTERESETPASSWORD',
87
            'Enter your e-mail address and we will send you a link with which you can reset your password'
88
        );
89
90
        return [
91
            'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
92
            'Form'    => $this->lostPasswordForm(),
93
        ];
94
    }
95
96
    /**
97
     * Show the "password sent" page, after a user has requested
98
     * to reset their password.
99
     *
100
     * @return array
101
     */
102
    public function passwordsent()
103
    {
104
        $request = $this->getRequest();
105
        $email = Convert::raw2xml(rawurldecode($request->param('EmailAddress')));
106
        if ($request->getExtension()) {
107
            $email = $email . '.' . Convert::raw2xml($request->getExtension());
108
        }
109
110
        $message = _t(
111
            'SilverStripe\\Security\\Security.PASSWORDSENTTEXT',
112
            "Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
113
            . " address.",
114
            ['email' => Convert::raw2xml($email)]
115
        );
116
117
        return [
118
            'Title'   => _t(
119
                'SilverStripe\\Security\\Security.PASSWORDSENTHEADER',
120
                "Password reset link sent to '{email}'",
121
                array('email' => $email)
122
            ),
123
            'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
124
            'Email'   => $email
125
        ];
126
    }
127
128
129
    /**
130
     * Factory method for the lost password form
131
     *
132
     * @skipUpgrade
133
     * @return Form Returns the lost password form
134
     */
135
    public function lostPasswordForm()
136
    {
137
        return LostPasswordForm::create(
138
            $this,
0 ignored issues
show
Bug introduced by
$this of type SilverStripe\Security\Me...tor\LostPasswordHandler is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

138
            /** @scrutinizer ignore-type */ $this,
Loading history...
139
            $this->authenticatorClass,
0 ignored issues
show
Bug introduced by
$this->authenticatorClass of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

139
            /** @scrutinizer ignore-type */ $this->authenticatorClass,
Loading history...
140
            'lostPasswordForm',
141
            null,
142
            null,
143
            false
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

143
            /** @scrutinizer ignore-type */ false
Loading history...
144
        );
145
    }
146
147
    /**
148
     * Redirect to password recovery form
149
     *
150
     * @return HTTPResponse
151
     */
152
    public function redirectToLostPassword()
153
    {
154
        $lostPasswordLink = Security::singleton()->Link('lostpassword');
155
156
        return $this->redirect($this->addBackURLParam($lostPasswordLink));
157
    }
158
159
    /**
160
     * Forgot password form handler method.
161
     * Called when the user clicks on "I've lost my password".
162
     * Extensions can use the 'forgotPassword' method to veto executing
163
     * the logic, by returning FALSE. In this case, the user will be redirected back
164
     * to the form without further action. It is recommended to set a message
165
     * in the form detailing why the action was denied.
166
     *
167
     * @skipUpgrade
168
     * @param array $data Submitted data
169
     * @param LostPasswordForm $form
170
     * @return HTTPResponse
171
     */
172
    public function forgotPassword($data, $form)
173
    {
174
        // Run a first pass validation check on the data
175
        $dataValidation = $this->validateForgotPasswordData($data, $form);
176
        if ($dataValidation instanceof HTTPResponse) {
177
            return $dataValidation;
178
        }
179
180
        /** @var Member $member */
181
        $member = $this->getMemberFromData($data);
182
183
        // Allow vetoing forgot password requests
184
        $results = $this->extend('forgotPassword', $member);
185
        if ($results && is_array($results) && in_array(false, $results, true)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
186
            return $this->redirectToLostPassword();
187
        }
188
189
        if ($member) {
190
            $token = $member->generateAutologinTokenAndStoreHash();
191
192
            $this->sendEmail($member, $token);
193
        }
194
195
        return $this->redirectToSuccess($data);
196
    }
197
198
    /**
199
     * Ensure that the user has provided an email address. Note that the "Email" key is specific to this
200
     * implementation, but child classes can override this method to use another unique identifier field
201
     * for validation.
202
     *
203
     * @param  array $data
204
     * @param  LostPasswordForm $form
205
     * @return HTTPResponse|null
206
     */
207
    protected function validateForgotPasswordData(array $data, LostPasswordForm $form)
208
    {
209
        if (empty($data['Email'])) {
210
            $form->sessionMessage(
211
                _t(
212
                    'SilverStripe\\Security\\Member.ENTEREMAIL',
213
                    'Please enter an email address to get a password reset link.'
214
                ),
215
                'bad'
216
            );
217
218
            return $this->redirectToLostPassword();
219
        }
220
    }
221
222
    /**
223
     * Load an existing Member from the provided data
224
     *
225
     * @param  array $data
226
     * @return Member|null
227
     */
228
    protected function getMemberFromData(array $data)
229
    {
230
        if (!empty($data['Email'])) {
231
            $uniqueIdentifier = Member::config()->get('unique_identifier_field');
232
            return Member::get()->filter([$uniqueIdentifier => $data['Email']])->first();
233
        }
234
    }
235
236
    /**
237
     * Send the email to the member that requested a reset link
238
     * @param Member $member
239
     * @param string $token
240
     * @return bool
241
     */
242
    protected function sendEmail($member, $token)
243
    {
244
        /** @var Email $email */
245
        $email = Email::create()
246
            ->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail')
247
            ->setData($member)
248
            ->setSubject(_t(
249
                'SilverStripe\\Security\\Member.SUBJECTPASSWORDRESET',
250
                "Your password reset link",
251
                'Email subject'
252
            ))
253
            ->addData('PasswordResetLink', Security::getPasswordResetLink($member, $token))
254
            ->setTo($member->Email);
255
        return $email->send();
256
    }
257
258
    /**
259
     * Avoid information disclosure by displaying the same status, regardless wether the email address actually exists
260
     *
261
     * @param array $data
262
     * @return HTTPResponse
263
     */
264
    protected function redirectToSuccess(array $data)
265
    {
266
        $link = Controller::join_links(
267
            $this->Link('passwordsent'),
268
            rawurlencode($data['Email']),
269
            '/'
270
        );
271
272
        return $this->redirect($this->addBackURLParam($link));
273
    }
274
}
275