Completed
Pull Request — master (#7028)
by Loz
12:53
created

SecurityTest::testLogout()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 53
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 27
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 53
rs 9.5797

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Dev\Debug;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\ORM\FieldType\DBDatetime;
8
use SilverStripe\ORM\FieldType\DBClassName;
9
use SilverStripe\ORM\DB;
10
use SilverStripe\ORM\ValidationResult;
11
use SilverStripe\Security\LoginAttempt;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Security\SecurityToken;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Core\Convert;
18
use SilverStripe\Dev\FunctionalTest;
19
use SilverStripe\Dev\TestOnly;
20
use SilverStripe\Control\HTTPResponse;
21
use SilverStripe\Control\Session;
22
use SilverStripe\Control\Director;
23
use SilverStripe\Control\Controller;
24
use SilverStripe\i18n\i18n;
25
26
/**
27
 * Test the security class, including log-in form, change password form, etc
28
 */
29
class SecurityTest extends FunctionalTest
30
{
31
    protected static $fixture_file = 'MemberTest.yml';
32
33
    protected $autoFollowRedirection = false;
34
35
    protected static $extra_controllers = [
36
        SecurityTest\NullController::class,
37
        SecurityTest\SecuredController::class,
38
    ];
39
40
    protected function setUp()
41
    {
42
        // Set to an empty array of authenticators to enable the default
43
        Config::modify()->set(MemberAuthenticator::class, 'authenticators', []);
44
        Config::modify()->set(MemberAuthenticator::class, 'default_authenticator', MemberAuthenticator::class);
45
46
        /**
47
         * @skipUpgrade
48
         */
49
        Member::config()->unique_identifier_field = 'Email';
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
50
51
        parent::setUp();
52
53
        Config::modify()->merge('SilverStripe\\Control\\Director', 'alternate_base_url', '/');
54
    }
55
56
    public function testAccessingAuthenticatedPageRedirectsToLoginForm()
57
    {
58
        $this->autoFollowRedirection = false;
59
60
        $response = $this->get('SecurityTest_SecuredController');
61
        $this->assertEquals(302, $response->getStatusCode());
62
        $this->assertContains(
63
            Config::inst()->get(Security::class, 'login_url'),
64
            $response->getHeader('Location')
65
        );
66
67
        $this->logInWithPermission('ADMIN');
68
        $response = $this->get('SecurityTest_SecuredController');
69
        $this->assertEquals(200, $response->getStatusCode());
70
        $this->assertContains('Success', $response->getBody());
71
72
        $this->autoFollowRedirection = true;
73
    }
74
75
    public function testPermissionFailureSetsCorrectFormMessages()
76
    {
77
        Config::nest();
78
79
        // Controller that doesn't attempt redirections
80
        $controller = new SecurityTest\NullController();
81
        $controller->setResponse(new HTTPResponse());
82
83
        Security::permissionFailure($controller, array('default' => 'Oops, not allowed'));
84
        $this->assertEquals('Oops, not allowed', Session::get('Security.Message.message'));
85
86
        // Test that config values are used correctly
87
        Config::inst()->update(Security::class, 'default_message_set', 'stringvalue');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
88
        Security::permissionFailure($controller);
89
        $this->assertEquals(
90
            'stringvalue',
91
            Session::get('Security.Message.message'),
92
            'Default permission failure message value was not present'
93
        );
94
95
        Config::modify()->remove(Security::class, 'default_message_set');
96
        Config::modify()->merge(Security::class, 'default_message_set', array('default' => 'arrayvalue'));
97
        Security::permissionFailure($controller);
98
        $this->assertEquals(
99
            'arrayvalue',
100
            Session::get('Security.Message.message'),
101
            'Default permission failure message value was not present'
102
        );
103
104
        // Test that non-default messages work.
105
        // NOTE: we inspect the response body here as the session message has already
106
        // been fetched and output as part of it, so has been removed from the session
107
        $this->logInWithPermission('EDITOR');
108
109
        Config::inst()->update(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
110
            Security::class,
111
            'default_message_set',
112
            array('default' => 'default', 'alreadyLoggedIn' => 'You are already logged in!')
113
        );
114
        Security::permissionFailure($controller);
115
        $this->assertContains(
116
            'You are already logged in!',
117
            $controller->getResponse()->getBody(),
118
            'Custom permission failure message was ignored'
119
        );
120
121
        Security::permissionFailure(
122
            $controller,
123
            array('default' => 'default', 'alreadyLoggedIn' => 'One-off failure message')
124
        );
125
        $this->assertContains(
126
            'One-off failure message',
127
            $controller->getResponse()->getBody(),
128
            "Message set passed to Security::permissionFailure() didn't override Config values"
129
        );
130
131
        Config::unnest();
132
    }
133
134
    /**
135
     * Follow all redirects recursively
136
     *
137
     * @param  string $url
138
     * @param  int    $limit Max number of requests
139
     * @return HTTPResponse
140
     */
141
    protected function getRecursive($url, $limit = 10)
142
    {
143
        $this->cssParser = null;
144
        $response = $this->mainSession->get($url);
145
        while (--$limit > 0 && $response instanceof HTTPResponse && $response->getHeader('Location')) {
146
            $response = $this->mainSession->followRedirection();
147
        }
148
        return $response;
149
    }
150
151
    public function testAutomaticRedirectionOnLogin()
152
    {
153
        // BackURL with permission error (not authenticated) should not redirect
154
        if ($member = Security::getCurrentUser()) {
155
            Security::setCurrentUser(null);
156
        }
157
        $response = $this->getRecursive('SecurityTest_SecuredController');
158
        $this->assertContains(Convert::raw2xml("That page is secured."), $response->getBody());
159
        $this->assertContains('<input type="submit" name="action_doLogin"', $response->getBody());
160
161
        // Non-logged in user should not be redirected, but instead shown the login form
162
        // No message/context is available as the user has not attempted to view the secured controller
163
        $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
164
        $this->assertNotContains(Convert::raw2xml("That page is secured."), $response->getBody());
165
        $this->assertNotContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
166
        $this->assertContains('<input type="submit" name="action_doLogin"', $response->getBody());
167
168
        // BackURL with permission error (wrong permissions) should not redirect
169
        $this->logInAs('grouplessmember');
170
        $response = $this->getRecursive('SecurityTest_SecuredController');
171
        $this->assertContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
172
        $this->assertContains(
173
            '<input type="submit" name="action_logout" value="Log in as someone else"',
174
            $response->getBody()
175
        );
176
177
        // Directly accessing this page should attempt to follow the BackURL, but stop when it encounters the error
178
        $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
179
        $this->assertContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
180
        $this->assertContains(
181
            '<input type="submit" name="action_logout" value="Log in as someone else"',
182
            $response->getBody()
183
        );
184
185
        // Check correctly logged in admin doesn't generate the same errors
186
        $this->logInAs('admin');
187
        $response = $this->getRecursive('SecurityTest_SecuredController');
188
        $this->assertContains(Convert::raw2xml("Success"), $response->getBody());
189
190
        // Directly accessing this page should attempt to follow the BackURL and succeed
191
        $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
192
        $this->assertContains(Convert::raw2xml("Success"), $response->getBody());
193
    }
194
195
    public function testLogInAsSomeoneElse()
196
    {
197
        $member = DataObject::get_one(Member::class);
198
199
        /* Log in with any user that we can find */
200
        Security::setCurrentUser($member);
201
202
        /* View the Security/login page */
203
        $response = $this->get(Config::inst()->get(Security::class, 'login_url'));
204
205
        $items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action');
206
207
        /* We have only 1 input, one to allow the user to log in as someone else */
208
        $this->assertEquals(count($items), 1, 'There is 1 input, allowing the user to log in as someone else.');
209
210
        $this->autoFollowRedirection = true;
211
212
        /* Submit the form, using only the logout action and a hidden field for the authenticator */
213
        $response = $this->submitForm(
214
            'MemberLoginForm_LoginForm',
215
            null,
216
            array(
217
                'action_logout' => 1,
218
            )
219
        );
220
221
        /* We get a good response */
222
        $this->assertEquals($response->getStatusCode(), 200, 'We have a 200 OK response');
223
        $this->assertNotNull($response->getBody(), 'There is body content on the page');
224
225
        /* Log the user out */
226
        Security::setCurrentUser(null);
227
    }
228
229
    public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin()
230
    {
231
        /* Log in with a Member ID that doesn't exist in the DB */
232
        $this->session()->inst_set('loggedInAs', 500);
233
234
        $this->autoFollowRedirection = true;
235
236
        /* Attempt to get into the admin section */
237
        $response = $this->get(Config::inst()->get(Security::class, 'login_url'));
238
239
        $items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text');
240
241
        /* We have 2 text inputs - one for email, and another for the password */
242
        $this->assertEquals(count($items), 2, 'There are 2 inputs - one for email, another for password');
243
244
        $this->autoFollowRedirection = false;
245
246
        /* Log the user out */
247
        $this->session()->inst_set('loggedInAs', null);
248
    }
249
250
    public function testLoginUsernamePersists()
251
    {
252
        // Test that username does not persist
253
        $this->session()->inst_set('SessionForms.MemberLoginForm.Email', '[email protected]');
254
        Security::config()->remember_username = false;
0 ignored issues
show
Documentation introduced by
The property remember_username does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
255
        $this->get(Config::inst()->get(Security::class, 'login_url'));
256
        $items = $this
257
            ->cssParser()
258
            ->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email');
259
        $this->assertEquals(1, count($items));
260
        $this->assertEmpty((string)$items[0]->attributes()->value);
261
        $this->assertEquals('off', (string)$items[0]->attributes()->autocomplete);
262
        $form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm');
263
        $this->assertEquals(1, count($form));
264
        $this->assertEquals('off', (string)$form[0]->attributes()->autocomplete);
265
266
        // Test that username does persist when necessary
267
        $this->session()->inst_set('SessionForms.MemberLoginForm.Email', '[email protected]');
268
        Security::config()->remember_username = true;
0 ignored issues
show
Documentation introduced by
The property remember_username does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
269
        $this->get(Config::inst()->get(Security::class, 'login_url'));
270
        $items = $this
271
            ->cssParser()
272
            ->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email');
273
        $this->assertEquals(1, count($items));
274
        $this->assertEquals('[email protected]', (string)$items[0]->attributes()->value);
275
        $this->assertNotEquals('off', (string)$items[0]->attributes()->autocomplete);
276
        $form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm');
277
        $this->assertEquals(1, count($form));
278
        $this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete);
279
    }
280
281
    public function testLogout()
282
    {
283
        /* Enable SecurityToken */
284
        $securityTokenWasEnabled = SecurityToken::is_enabled();
285
        SecurityToken::enable();
286
287
        $member = DataObject::get_one(Member::class);
288
289
        /* Log in with any user that we can find */
290
        Security::setCurrentUser($member);
291
292
        /* Visit the Security/logout page with a test referer, but without a security token */
293
        $response = $this->get(
294
            Config::inst()->get(Security::class, 'logout_url'),
295
            null,
296
            ['Referer' => Director::absoluteBaseURL() . 'testpage']
297
        );
298
299
        /* Make sure the user is still logged in */
300
        $this->assertNotNull(Security::getCurrentUser(), 'User is still logged in.');
301
302
        $token = $this->cssParser()->getBySelector('#LogoutForm_Form #LogoutForm_Form_SecurityID');
303
        $actions = $this->cssParser()->getBySelector('#LogoutForm_Form input.action');
304
305
        /* We have a security token, and an action to allow the user to log out */
306
        $this->assertCount(1, $token, 'There is a hidden field containing a security token.');
307
        $this->assertCount(1, $actions, 'There is 1 action, allowing the user to log out.');
308
309
        /* Submit the form, using the logout action */
310
        $response = $this->submitForm(
311
            'LogoutForm_Form',
312
            null,
313
            array(
314
                'action_doLogout' => 1,
315
            )
316
        );
317
318
        /* We get a good response */
319
        $this->assertEquals(302, $response->getStatusCode());
320
        $this->assertRegExp(
321
            '/testpage/',
322
            $response->getHeader('Location'),
323
            "Logout form redirects to back to referer."
324
        );
325
326
        /* User is logged out successfully */
327
        $this->assertNull(Security::getCurrentUser(), 'User is logged out.');
328
329
        /* Re-disable SecurityToken */
330
        if (!$securityTokenWasEnabled) {
331
            SecurityToken::disable();
332
        }
333
    }
334
335
    public function testExternalBackUrlRedirectionDisallowed()
336
    {
337
        // Test internal relative redirect
338
        $response = $this->doTestLoginForm('[email protected]', '1nitialPassword', 'testpage');
339
        $this->assertEquals(302, $response->getStatusCode());
340
        $this->assertRegExp(
341
            '/testpage/',
342
            $response->getHeader('Location'),
343
            "Internal relative BackURLs work when passed through to login form"
344
        );
345
        // Log the user out
346
        $this->session()->inst_set('loggedInAs', null);
347
348
        // Test internal absolute redirect
349
        $response = $this->doTestLoginForm(
350
            '[email protected]',
351
            '1nitialPassword',
352
            Director::absoluteBaseURL() . 'testpage'
353
        );
354
        // for some reason the redirect happens to a relative URL
355
        $this->assertRegExp(
356
            '/^' . preg_quote(Director::absoluteBaseURL(), '/') . 'testpage/',
357
            $response->getHeader('Location'),
358
            "Internal absolute BackURLs work when passed through to login form"
359
        );
360
        // Log the user out
361
        $this->session()->inst_set('loggedInAs', null);
362
363
        // Test external redirect
364
        $response = $this->doTestLoginForm('[email protected]', '1nitialPassword', 'http://myspoofedhost.com');
365
        $this->assertNotRegExp(
366
            '/^' . preg_quote('http://myspoofedhost.com', '/') . '/',
367
            (string)$response->getHeader('Location'),
368
            "Redirection to external links in login form BackURL gets prevented as a measure against spoofing attacks"
369
        );
370
371
        // Test external redirection on ChangePasswordForm
372
        $this->get('Security/changepassword?BackURL=http://myspoofedhost.com');
373
        $changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
374
        $this->assertNotRegExp(
375
            '/^' . preg_quote('http://myspoofedhost.com', '/') . '/',
376
            (string)$changedResponse->getHeader('Location'),
377
            "Redirection to external links in change password form BackURL gets prevented to stop spoofing attacks"
378
        );
379
380
        // Log the user out
381
        $this->session()->inst_set('loggedInAs', null);
382
    }
383
384
    /**
385
     * Test that the login form redirects to the change password form after logging in with an expired password
386
     */
387
    public function testExpiredPassword()
388
    {
389
        /* BAD PASSWORDS ARE LOCKED OUT */
390
        $badResponse = $this->doTestLoginForm('[email protected]', 'badpassword');
391
        $this->assertEquals(302, $badResponse->getStatusCode());
392
        $this->assertRegExp('/Security\/login/', $badResponse->getHeader('Location'));
393
        $this->assertNull($this->session()->inst_get('loggedInAs'));
394
395
        /* UNEXPIRED PASSWORD GO THROUGH WITHOUT A HITCH */
396
        $goodResponse = $this->doTestLoginForm('[email protected]', '1nitialPassword');
397
        $this->assertEquals(302, $goodResponse->getStatusCode());
398
        $this->assertEquals(
399
            Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
400
            $goodResponse->getHeader('Location')
401
        );
402
        $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
403
404
        $this->logOut();
405
406
        /* EXPIRED PASSWORDS ARE SENT TO THE CHANGE PASSWORD FORM */
407
        $expiredResponse = $this->doTestLoginForm('[email protected]', '1nitialPassword');
408
        $this->assertEquals(302, $expiredResponse->getStatusCode());
409
        $this->assertEquals(
410
            Director::absoluteURL('Security/changepassword').'?BackURL=test%2Flink',
411
            Director::absoluteURL($expiredResponse->getHeader('Location'))
412
        );
413
        $this->assertEquals(
414
            $this->idFromFixture(Member::class, 'expiredpassword'),
415
            $this->session()->inst_get('loggedInAs')
416
        );
417
418
        // Make sure it redirects correctly after the password has been changed
419
        $this->mainSession->followRedirection();
420
        $changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
421
        $this->assertEquals(302, $changedResponse->getStatusCode());
422
        $this->assertEquals(
423
            Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
424
            $changedResponse->getHeader('Location')
425
        );
426
    }
427
428
    public function testChangePasswordForLoggedInUsers()
429
    {
430
        $goodResponse = $this->doTestLoginForm('[email protected]', '1nitialPassword');
431
432
        // Change the password
433
        $this->get('Security/changepassword?BackURL=test/back');
434
        $changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
435
        $this->assertEquals(302, $changedResponse->getStatusCode());
436
        $this->assertEquals(
437
            Controller::join_links(Director::absoluteBaseURL(), 'test/back'),
438
            $changedResponse->getHeader('Location')
439
        );
440
        $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
441
442
        // Check if we can login with the new password
443
        $this->logOut();
444
        $goodResponse = $this->doTestLoginForm('[email protected]', 'changedPassword');
445
        $this->assertEquals(302, $goodResponse->getStatusCode());
446
        $this->assertEquals(
447
            Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
448
            $goodResponse->getHeader('Location')
449
        );
450
        $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
451
    }
452
453
    public function testChangePasswordFromLostPassword()
454
    {
455
        $admin = $this->objFromFixture(Member::class, 'test');
456
        $admin->FailedLoginCount = 99;
0 ignored issues
show
Documentation introduced by
The property FailedLoginCount does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
457
        $admin->LockedOutUntil = DBDatetime::now()->getValue();
0 ignored issues
show
Documentation introduced by
The property LockedOutUntil does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
458
        $admin->write();
459
460
        $this->assertNull($admin->AutoLoginHash, 'Hash is empty before lost password');
461
462
        // Request new password by email
463
        $response = $this->get('Security/lostpassword');
464
        $response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => '[email protected]'));
465
466
        $this->assertEmailSent('[email protected]');
467
468
        // Load password link from email
469
        $admin = DataObject::get_by_id(Member::class, $admin->ID);
470
        $this->assertNotNull($admin->AutoLoginHash, 'Hash has been written after lost password');
471
472
        // We don't have access to the token - generate a new token and hash pair.
473
        $token = $admin->generateAutologinTokenAndStoreHash();
474
475
        // Check.
476
        $response = $this->get('Security/changepassword/?m='.$admin->ID.'&t=' . $token);
477
        $this->assertEquals(302, $response->getStatusCode());
478
        $this->assertEquals(
479
            Director::absoluteURL('Security/changepassword'),
480
            Director::absoluteURL($response->getHeader('Location'))
481
        );
482
483
        // Follow redirection to form without hash in GET parameter
484
        $response = $this->get('Security/changepassword');
485
        $changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
486
        $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
487
488
        // Check if we can login with the new password
489
        $this->logOut();
490
        $goodResponse = $this->doTestLoginForm('[email protected]', 'changedPassword');
491
        $this->assertEquals(302, $goodResponse->getStatusCode());
492
        $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
493
494
        $admin = DataObject::get_by_id(Member::class, $admin->ID, false);
495
        $this->assertNull($admin->LockedOutUntil);
496
        $this->assertEquals(0, $admin->FailedLoginCount);
497
    }
498
499
    public function testRepeatedLoginAttemptsLockingPeopleOut()
500
    {
501
        $local = i18n::get_locale();
502
        i18n::set_locale('en_US');
503
504
        Member::config()->lock_out_after_incorrect_logins = 5;
0 ignored issues
show
Documentation introduced by
The property lock_out_after_incorrect_logins does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
505
        Member::config()->lock_out_delay_mins = 15;
0 ignored issues
show
Documentation introduced by
The property lock_out_delay_mins does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
506
507
        // Login with a wrong password for more than the defined threshold
508
        for ($i = 1; $i <= (Member::config()->lock_out_after_incorrect_logins+1); $i++) {
509
            $this->doTestLoginForm('[email protected]', 'incorrectpassword');
510
            $member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
511
512
            if ($i < Member::config()->get('lock_out_after_incorrect_logins')) {
513
                $this->assertNull(
514
                    $member->LockedOutUntil,
515
                    'User does not have a lockout time set if under threshold for failed attempts'
516
                );
517
                $this->assertHasMessage(
518
                    _t(
519
                        'SilverStripe\\Security\\Member.ERRORWRONGCRED',
520
                        'The provided details don\'t seem to be correct. Please try again.'
521
                    )
522
                );
523
            } else {
524
                // Fuzzy matching for time to avoid side effects from slow running tests
525
                $this->assertGreaterThan(
526
                    time() + 14*60,
527
                    strtotime($member->LockedOutUntil),
528
                    'User has a lockout time set after too many failed attempts'
529
                );
530
            }
531
        }
532
        $msg = _t(
533
            'SilverStripe\\Security\\Member.ERRORLOCKEDOUT2',
534
            'Your account has been temporarily disabled because of too many failed attempts at ' .
535
            'logging in. Please try again in {count} minutes.',
536
            null,
537
            array('count' => Member::config()->lock_out_delay_mins)
538
        );
539
        $this->assertHasMessage($msg);
540
541
542
        $this->doTestLoginForm('[email protected]', '1nitialPassword');
543
        $this->assertNull(
544
            $this->session()->inst_get('loggedInAs'),
545
            'The user can\'t log in after being locked out, even with the right password'
546
        );
547
548
        // (We fake this by re-setting LockedOutUntil)
549
        $member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
550
        $member->LockedOutUntil = date('Y-m-d H:i:s', time() - 30);
0 ignored issues
show
Documentation introduced by
The property LockedOutUntil does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
551
        $member->write();
552
        $this->doTestLoginForm('[email protected]', '1nitialPassword');
553
        $this->assertEquals(
554
            $this->session()->inst_get('loggedInAs'),
555
            $member->ID,
556
            'After lockout expires, the user can login again'
557
        );
558
559
        // Log the user out
560
        $this->logOut();
561
562
        // Login again with wrong password, but less attempts than threshold
563
        for ($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) {
564
            $this->doTestLoginForm('[email protected]', 'incorrectpassword');
565
        }
566
        $this->assertNull($this->session()->inst_get('loggedInAs'));
567
        $this->assertHasMessage(
568
            _t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'),
569
            'The user can retry with a wrong password after the lockout expires'
570
        );
571
572
        $this->doTestLoginForm('[email protected]', '1nitialPassword');
573
        $this->assertEquals(
574
            $this->session()->inst_get('loggedInAs'),
575
            $member->ID,
576
            'The user can login successfully after lockout expires, if staying below the threshold'
577
        );
578
579
        i18n::set_locale($local);
580
    }
581
582
    public function testAlternatingRepeatedLoginAttempts()
583
    {
584
        Member::config()->lock_out_after_incorrect_logins = 3;
0 ignored issues
show
Documentation introduced by
The property lock_out_after_incorrect_logins does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
585
586
        // ATTEMPTING LOG-IN TWICE WITH ONE ACCOUNT AND TWICE WITH ANOTHER SHOULDN'T LOCK ANYBODY OUT
587
588
        $this->doTestLoginForm('[email protected]', 'incorrectpassword');
589
        $this->doTestLoginForm('[email protected]', 'incorrectpassword');
590
591
        $this->doTestLoginForm('[email protected]', 'incorrectpassword');
592
        $this->doTestLoginForm('[email protected]', 'incorrectpassword');
593
594
        $member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
595
        $member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry'));
596
597
        $this->assertNull($member1->LockedOutUntil);
598
        $this->assertNull($member2->LockedOutUntil);
599
600
        // BUT, DOING AN ADDITIONAL LOG-IN WITH EITHER OF THEM WILL LOCK OUT, SINCE THAT IS THE 3RD FAILURE IN
601
        // THIS SESSION
602
603
        $this->doTestLoginForm('[email protected]', 'incorrectpassword');
604
        $member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
605
        $this->assertNotNull($member1->LockedOutUntil);
606
607
        $this->doTestLoginForm('[email protected]', 'incorrectpassword');
608
        $member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry'));
609
        $this->assertNotNull($member2->LockedOutUntil);
610
    }
611
612
    public function testUnsuccessfulLoginAttempts()
613
    {
614
        Security::config()->login_recording = true;
0 ignored issues
show
Documentation introduced by
The property login_recording does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
615
616
        /* UNSUCCESSFUL ATTEMPTS WITH WRONG PASSWORD FOR EXISTING USER ARE LOGGED */
617
        $this->doTestLoginForm('[email protected]', 'wrongpassword');
618
        $attempt = DataObject::get_one(
619
            LoginAttempt::class,
620
            array(
621
                '"LoginAttempt"."Email"' => '[email protected]'
622
            )
623
        );
624
        $this->assertTrue(is_object($attempt));
625
        $member = DataObject::get_one(
626
            Member::class,
627
            array(
628
                '"Member"."Email"' => '[email protected]'
629
            )
630
        );
631
        $this->assertEquals($attempt->Status, 'Failure');
632
        $this->assertEquals($attempt->Email, '[email protected]');
633
        $this->assertEquals($attempt->Member()->toMap(), $member->toMap());
634
635
        /* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */
636
        $this->doTestLoginForm('[email protected]', 'wrongpassword');
637
        $attempt = DataObject::get_one(
638
            LoginAttempt::class,
639
            array(
640
            '"LoginAttempt"."Email"' => '[email protected]'
641
            )
642
        );
643
        $this->assertTrue(is_object($attempt));
644
        $this->assertEquals($attempt->Status, 'Failure');
645
        $this->assertEquals($attempt->Email, '[email protected]');
646
        $this->assertNotEmpty($this->getValidationResult()->getMessages(), 'An invalid email returns a message.');
647
    }
648
649
    public function testSuccessfulLoginAttempts()
650
    {
651
        Security::config()->login_recording = true;
0 ignored issues
show
Documentation introduced by
The property login_recording does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
652
653
        /* SUCCESSFUL ATTEMPTS ARE LOGGED */
654
        $this->doTestLoginForm('[email protected]', '1nitialPassword');
655
        $attempt = DataObject::get_one(
656
            LoginAttempt::class,
657
            array(
658
            '"LoginAttempt"."Email"' => '[email protected]'
659
            )
660
        );
661
        $member = DataObject::get_one(
662
            Member::class,
663
            array(
664
            '"Member"."Email"' => '[email protected]'
665
            )
666
        );
667
        $this->assertTrue(is_object($attempt));
668
        $this->assertEquals($attempt->Status, 'Success');
669
        $this->assertEquals($attempt->Email, '[email protected]');
670
        $this->assertEquals($attempt->Member()->toMap(), $member->toMap());
671
    }
672
673
    public function testDatabaseIsReadyWithInsufficientMemberColumns()
674
    {
675
        Security::clear_database_is_ready();
676
        DBClassName::clear_classname_cache();
677
678
        // Assumption: The database has been built correctly by the test runner,
679
        // and has all columns present in the ORM
680
        /**
681
         * @skipUpgrade
682
         */
683
        DB::get_schema()->renameField('Member', 'Email', 'Email_renamed');
684
685
        // Email column is now missing, which means we're not ready to do permission checks
686
        $this->assertFalse(Security::database_is_ready());
687
688
        // Rebuild the database (which re-adds the Email column), and try again
689
        static::resetDBSchema(true);
690
        $this->assertTrue(Security::database_is_ready());
691
    }
692
693
    public function testSecurityControllerSendsRobotsTagHeader()
694
    {
695
        $response = $this->get(Config::inst()->get(Security::class, 'login_url'));
696
        $robotsHeader = $response->getHeader('X-Robots-Tag');
697
        $this->assertNotNull($robotsHeader);
698
        $this->assertContains('noindex', $robotsHeader);
699
    }
700
701
    public function testDoNotSendEmptyRobotsHeaderIfNotDefined()
702
    {
703
        Config::inst()->remove(Security::class, 'robots_tag');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method remove() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
704
        $response = $this->get(Config::inst()->get(Security::class, 'login_url'));
705
        $robotsHeader = $response->getHeader('X-Robots-Tag');
706
        $this->assertNull($robotsHeader);
707
    }
708
709
    /**
710
     * Execute a log-in form using Director::test().
711
     * Helper method for the tests above
712
     */
713
    public function doTestLoginForm($email, $password, $backURL = 'test/link')
714
    {
715
        $this->get(Config::inst()->get(Security::class, 'logout_url'));
716
        $this->session()->inst_set('BackURL', $backURL);
717
        $this->get(Config::inst()->get(Security::class, 'login_url'));
718
719
        return $this->submitForm(
720
            "MemberLoginForm_LoginForm",
721
            null,
722
            array(
723
                'Email' => $email,
724
                'Password' => $password,
725
                'AuthenticationMethod' => MemberAuthenticator::class,
726
                'action_doLogin' => 1,
727
            )
728
        );
729
    }
730
731
    /**
732
     * Helper method to execute a change password form
733
     */
734
    public function doTestChangepasswordForm($oldPassword, $newPassword)
735
    {
736
        return $this->submitForm(
737
            "ChangePasswordForm_ChangePasswordForm",
738
            null,
739
            array(
740
                'OldPassword' => $oldPassword,
741
                'NewPassword1' => $newPassword,
742
                'NewPassword2' => $newPassword,
743
                'action_doChangePassword' => 1,
744
            )
745
        );
746
    }
747
748
    /**
749
     * Assert this message is in the current login form errors
750
     *
751
     * @param string $expected
752
     * @param string $errorMessage
753
     */
754
    protected function assertHasMessage($expected, $errorMessage = null)
755
    {
756
        $messages = [];
757
        $result = $this->getValidationResult();
758
        if ($result) {
759
            foreach ($result->getMessages() as $message) {
760
                $messages[] = $message['message'];
761
            }
762
        }
763
764
        $this->assertContains($expected, $messages, $errorMessage);
765
    }
766
767
    /**
768
     * Get validation result from last login form submission
769
     *
770
     * @return ValidationResult
771
     */
772
    protected function getValidationResult()
773
    {
774
        $result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result');
775
        if ($result) {
776
            return unserialize($result);
777
        }
778
        return null;
779
    }
780
}
781