Completed
Pull Request — master (#6498)
by Damian
09:19
created

MemberTest::addExtensions()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Core\Convert;
6
use SilverStripe\Core\Object;
7
use SilverStripe\Dev\FunctionalTest;
8
use SilverStripe\Control\Cookie;
9
use SilverStripe\i18n\i18n;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\ORM\DB;
12
use SilverStripe\ORM\FieldType\DBDatetime;
13
use SilverStripe\Security\Member;
14
use SilverStripe\Security\MemberAuthenticator;
15
use SilverStripe\Security\Security;
16
use SilverStripe\Security\MemberPassword;
17
use SilverStripe\Security\Group;
18
use SilverStripe\Security\Permission;
19
use SilverStripe\Security\PasswordEncryptor_Blowfish;
20
use SilverStripe\Security\RememberLoginHash;
21
use SilverStripe\Security\Member_Validator;
22
use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
23
24
class MemberTest extends FunctionalTest
25
{
26
    protected static $fixture_file = 'MemberTest.yml';
27
28
    protected $orig = array();
29
    protected $local = null;
30
31
    protected $illegalExtensions = array(
32
        Member::class => array(
33
            // TODO Coupling with modules, this should be resolved by automatically
34
            // removing all applied extensions before a unit test
35
            'ForumRole',
36
            'OpenIDAuthenticatedRole'
37
        )
38
    );
39
40
    public function __construct()
41
    {
42
        parent::__construct();
43
44
        //Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
45
        //This is because the test relies on the yaml file being interpreted according to a particular date format
46
        //and this setup occurs before the setUp method is run
47
        $this->local = i18n::config()->get('default_locale');
48
        i18n::config()->update('default_locale', 'en_US');
49
    }
50
51
    public function __destruct()
52
    {
53
        i18n::config()->update('default_locale', $this->local);
54
    }
55
56
    /**
57
     * @skipUpgrade
58
     */
59
    public function setUp()
60
    {
61
        parent::setUp();
62
63
        $this->orig['Member_unique_identifier_field'] = Member::config()->unique_identifier_field;
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...
64
        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...
65
        Member::set_password_validator(null);
66
    }
67
68
    public function tearDown()
69
    {
70
        Member::config()->unique_identifier_field = $this->orig['Member_unique_identifier_field'];
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...
71
        parent::tearDown();
72
    }
73
74
75
76
    /**
77
     * @expectedException \SilverStripe\ORM\ValidationException
78
     */
79
    public function testWriteDoesntMergeNewRecordWithExistingMember()
80
    {
81
        $m1 = new Member();
82
        $m1->Email = '[email protected]';
83
        $m1->write();
84
85
        $m2 = new Member();
86
        $m2->Email = '[email protected]';
87
        $m2->write();
88
    }
89
90
    /**
91
     * @expectedException \SilverStripe\ORM\ValidationException
92
     */
93
    public function testWriteDoesntMergeExistingMemberOnIdentifierChange()
94
    {
95
        $m1 = new Member();
96
        $m1->Email = '[email protected]';
97
        $m1->write();
98
99
        $m2 = new Member();
100
        $m2->Email = '[email protected]';
101
        $m2->write();
102
103
        $m2->Email = '[email protected]';
104
        $m2->write();
105
    }
106
107
    public function testDefaultPasswordEncryptionOnMember()
108
    {
109
        $memberWithPassword = new Member();
110
        $memberWithPassword->Password = 'mypassword';
111
        $memberWithPassword->write();
112
        $this->assertEquals(
113
            $memberWithPassword->PasswordEncryption,
114
            Security::config()->password_encryption_algorithm,
115
            'Password encryption is set for new member records on first write (with setting "Password")'
116
        );
117
118
        $memberNoPassword = new Member();
119
        $memberNoPassword->write();
120
        $this->assertNull(
121
            $memberNoPassword->PasswordEncryption,
122
            'Password encryption is not set for new member records on first write, when not setting a "Password")'
123
        );
124
    }
125
126
    public function testDefaultPasswordEncryptionDoesntChangeExistingMembers()
127
    {
128
        $member = new Member();
129
        $member->Password = 'mypassword';
130
        $member->PasswordEncryption = 'sha1_v2.4';
131
        $member->write();
132
133
        $origAlgo = Security::config()->password_encryption_algorithm;
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm 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...
134
        Security::config()->password_encryption_algorithm = 'none';
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm 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...
135
136
        $member->Password = 'mynewpassword';
137
        $member->write();
138
139
        $this->assertEquals(
140
            $member->PasswordEncryption,
141
            'sha1_v2.4'
142
        );
143
        $result = $member->checkPassword('mynewpassword');
144
        $this->assertTrue($result->isValid());
145
146
        Security::config()->password_encryption_algorithm = $origAlgo;
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm 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...
147
    }
148
149
    public function testKeepsEncryptionOnEmptyPasswords()
150
    {
151
        $member = new Member();
152
        $member->Password = 'mypassword';
153
        $member->PasswordEncryption = 'sha1_v2.4';
154
        $member->write();
155
156
        $member->Password = '';
157
        $member->write();
158
159
        $this->assertEquals(
160
            $member->PasswordEncryption,
161
            'sha1_v2.4'
162
        );
163
        $result = $member->checkPassword('');
164
        $this->assertTrue($result->isValid());
165
    }
166
167
    public function testSetPassword()
168
    {
169
        $member = $this->objFromFixture(Member::class, 'test');
170
        $member->Password = "test1";
0 ignored issues
show
Documentation introduced by
The property Password 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...
171
        $member->write();
172
        $result = $member->checkPassword('test1');
173
        $this->assertTrue($result->isValid());
174
    }
175
176
    /**
177
     * Test that password changes are logged properly
178
     */
179
    public function testPasswordChangeLogging()
180
    {
181
        $member = $this->objFromFixture(Member::class, 'test');
182
        $this->assertNotNull($member);
183
        $member->Password = "test1";
0 ignored issues
show
Documentation introduced by
The property Password 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...
184
        $member->write();
185
186
        $member->Password = "test2";
0 ignored issues
show
Documentation introduced by
The property Password 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...
187
        $member->write();
188
189
        $member->Password = "test3";
0 ignored issues
show
Documentation introduced by
The property Password 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...
190
        $member->write();
191
192
        $passwords = DataObject::get("SilverStripe\\Security\\MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
193
            ->getIterator();
194
        $this->assertNotNull($passwords);
195
        $passwords->rewind();
196
        $this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
197
198
        $passwords->next();
199
        $this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
200
201
        $passwords->next();
202
        $this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
203
204
        $passwords->next();
205
        $this->assertInstanceOf('SilverStripe\\ORM\\DataObject', $passwords->current());
206
        $this->assertTrue(
207
            $passwords->current()->checkPassword('1nitialPassword'),
208
            "Password 1nitialPassword not found in MemberRecord"
209
        );
210
211
        //check we don't retain orphaned records when a member is deleted
212
        $member->delete();
213
214
        $passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
215
216
        $this->assertCount(0, $passwords);
217
    }
218
219
    /**
220
     * Test that changed passwords will send an email
221
     */
222
    public function testChangedPasswordEmaling()
223
    {
224
        Member::config()->update('notify_password_change', true);
225
226
        $this->clearEmails();
227
228
        $member = $this->objFromFixture(Member::class, 'test');
229
        $this->assertNotNull($member);
230
        $valid = $member->changePassword('32asDF##$$%%');
231
        $this->assertTrue($valid->isValid());
232
233
        $this->assertEmailSent(
234
            '[email protected]',
235
            null,
236
            'Your password has been changed',
237
            '/testuser@example\.com/'
238
        );
239
    }
240
241
    /**
242
     * Test that triggering "forgotPassword" sends an Email with a reset link
243
        */
244
    public function testForgotPasswordEmaling()
245
    {
246
        $this->clearEmails();
247
        $this->autoFollowRedirection = false;
248
249
        $member = $this->objFromFixture(Member::class, 'test');
250
        $this->assertNotNull($member);
251
252
        // Initiate a password-reset
253
        $response = $this->post('Security/LostPasswordForm', array('Email' => $member->Email));
254
255
        $this->assertEquals($response->getStatusCode(), 302);
256
257
        // We should get redirected to Security/passwordsent
258
        $this->assertContains(
259
            'Security/passwordsent/[email protected]',
260
            urldecode($response->getHeader('Location'))
261
        );
262
263
        // Check existance of reset link
264
        $this->assertEmailSent(
265
            "[email protected]",
266
            null,
267
            'Your password reset link',
268
            '/Security\/changepassword\?m='.$member->ID.'&amp;t=[^"]+/'
269
        );
270
    }
271
272
    /**
273
     * Test that passwords validate against NZ e-government guidelines
274
     *  - don't allow the use of the last 6 passwords
275
     *  - require at least 3 of lowercase, uppercase, digits and punctuation
276
     *  - at least 7 characters long
277
     */
278
    public function testValidatePassword()
279
    {
280
        /**
281
 * @var Member $member
282
*/
283
        $member = $this->objFromFixture(Member::class, 'test');
284
        $this->assertNotNull($member);
285
286
        Member::set_password_validator(new MemberTest\TestPasswordValidator());
287
288
        // BAD PASSWORDS
289
290
        $result = $member->changePassword('shorty');
291
        $this->assertFalse($result->isValid());
292
        $this->assertArrayHasKey("TOO_SHORT", $result->getMessages());
293
294
        $result = $member->changePassword('longone');
295
        $this->assertArrayNotHasKey("TOO_SHORT", $result->getMessages());
296
        $this->assertArrayHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
297
        $this->assertFalse($result->isValid());
298
299
        $result = $member->changePassword('w1thNumb3rs');
300
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
301
        $this->assertTrue($result->isValid());
302
303
        // Clear out the MemberPassword table to ensure that the system functions properly in that situation
304
        DB::query("DELETE FROM \"MemberPassword\"");
305
306
        // GOOD PASSWORDS
307
308
        $result = $member->changePassword('withSym###Ls');
309
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
310
        $this->assertTrue($result->isValid());
311
312
        $result = $member->changePassword('withSym###Ls2');
313
        $this->assertTrue($result->isValid());
314
315
        $result = $member->changePassword('withSym###Ls3');
316
        $this->assertTrue($result->isValid());
317
318
        $result = $member->changePassword('withSym###Ls4');
319
        $this->assertTrue($result->isValid());
320
321
        $result = $member->changePassword('withSym###Ls5');
322
        $this->assertTrue($result->isValid());
323
324
        $result = $member->changePassword('withSym###Ls6');
325
        $this->assertTrue($result->isValid());
326
327
        $result = $member->changePassword('withSym###Ls7');
328
        $this->assertTrue($result->isValid());
329
330
        // CAN'T USE PASSWORDS 2-7, but I can use pasword 1
331
332
        $result = $member->changePassword('withSym###Ls2');
333
        $this->assertFalse($result->isValid());
334
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
335
336
        $result = $member->changePassword('withSym###Ls5');
337
        $this->assertFalse($result->isValid());
338
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
339
340
        $result = $member->changePassword('withSym###Ls7');
341
        $this->assertFalse($result->isValid());
342
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
343
344
        $result = $member->changePassword('withSym###Ls');
345
        $this->assertTrue($result->isValid());
346
347
        // HAVING DONE THAT, PASSWORD 2 is now available from the list
348
349
        $result = $member->changePassword('withSym###Ls2');
350
        $this->assertTrue($result->isValid());
351
352
        $result = $member->changePassword('withSym###Ls3');
353
        $this->assertTrue($result->isValid());
354
355
        $result = $member->changePassword('withSym###Ls4');
356
        $this->assertTrue($result->isValid());
357
358
        Member::set_password_validator(null);
359
    }
360
361
    /**
362
     * Test that the PasswordExpiry date is set when passwords are changed
363
     */
364
    public function testPasswordExpirySetting()
365
    {
366
        Member::config()->password_expiry_days = 90;
0 ignored issues
show
Documentation introduced by
The property password_expiry_days 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...
367
368
        $member = $this->objFromFixture(Member::class, 'test');
369
        $this->assertNotNull($member);
370
        $valid = $member->changePassword("Xx?1234234");
371
        $this->assertTrue($valid->isValid());
372
373
        $expiryDate = date('Y-m-d', time() + 90*86400);
374
        $this->assertEquals($expiryDate, $member->PasswordExpiry);
375
376
        Member::config()->password_expiry_days = null;
0 ignored issues
show
Documentation introduced by
The property password_expiry_days 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...
377
        $valid = $member->changePassword("Xx?1234235");
378
        $this->assertTrue($valid->isValid());
379
380
        $this->assertNull($member->PasswordExpiry);
381
    }
382
383
    public function testIsPasswordExpired()
384
    {
385
        $member = $this->objFromFixture(Member::class, 'test');
386
        $this->assertNotNull($member);
387
        $this->assertFalse($member->isPasswordExpired());
388
389
        $member = $this->objFromFixture(Member::class, 'noexpiry');
390
        $member->PasswordExpiry = null;
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry 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...
391
        $this->assertFalse($member->isPasswordExpired());
392
393
        $member = $this->objFromFixture(Member::class, 'expiredpassword');
394
        $this->assertTrue($member->isPasswordExpired());
395
396
        // Check the boundary conditions
397
        // If PasswordExpiry == today, then it's expired
398
        $member->PasswordExpiry = date('Y-m-d');
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry 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...
399
        $this->assertTrue($member->isPasswordExpired());
400
401
        // If PasswordExpiry == tomorrow, then it's not
402
        $member->PasswordExpiry = date('Y-m-d', time() + 86400);
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry 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...
403
        $this->assertFalse($member->isPasswordExpired());
404
    }
405
406
    public function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat()
407
    {
408
        i18n::config()
409
            ->update('date_format', 'yyyy-MM-dd')
410
            ->update('time_format', 'H:mm');
411
        $member = $this->objFromFixture(Member::class, 'noformatmember');
412
        $this->assertEquals('yyyy-MM-dd', $member->DateFormat);
413
        $this->assertEquals('H:mm', $member->TimeFormat);
414
    }
415
416
    public function testInGroups()
417
    {
418
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
419
        $managementmember = $this->objFromFixture(Member::class, 'managementmember');
420
        $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
421
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
422
423
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
424
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
425
        $accountinggroup = $this->objFromFixture(Group::class, 'accountinggroup');
426
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
427
428
        $this->assertTrue(
429
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
430
            'inGroups() succeeds if a membership is detected on one of many passed groups'
431
        );
432
        $this->assertFalse(
433
            $staffmember->inGroups(array($ceogroup, $managementgroup)),
434
            'inGroups() fails if a membership is detected on none of the passed groups'
435
        );
436
        $this->assertFalse(
437
            $ceomember->inGroups(array($staffgroup, $managementgroup), true),
438
            'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
439
        );
440
    }
441
442
    public function testAddToGroupByCode()
443
    {
444
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
445
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
446
447
        $this->assertFalse($grouplessMember->Groups()->exists());
448
        $this->assertFalse($memberlessGroup->Members()->exists());
449
450
        $grouplessMember->addToGroupByCode('memberless');
451
452
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
453
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
454
455
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
456
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
457
458
        $group = DataObject::get_one(
459
            Group::class,
460
            array(
461
            '"Group"."Code"' => 'somegroupthatwouldneverexist'
462
            )
463
        );
464
        $this->assertNotNull($group);
465
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
466
        $this->assertEquals($group->Title, 'New Group');
467
    }
468
469
    public function testRemoveFromGroupByCode()
470
    {
471
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
472
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
473
474
        $this->assertFalse($grouplessMember->Groups()->exists());
475
        $this->assertFalse($memberlessGroup->Members()->exists());
476
477
        $grouplessMember->addToGroupByCode('memberless');
478
479
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
480
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
481
482
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
483
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
484
485
        $group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
486
        $this->assertNotNull($group);
487
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
488
        $this->assertEquals($group->Title, 'New Group');
489
490
        $grouplessMember->removeFromGroupByCode('memberless');
491
        $this->assertEquals($memberlessGroup->Members()->count(), 0);
492
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
493
494
        $grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
495
        $this->assertEquals($grouplessMember->Groups()->count(), 0);
496
    }
497
498
    public function testInGroup()
499
    {
500
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
501
        $managementmember = $this->objFromFixture(Member::class, 'managementmember');
502
        $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
503
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
504
505
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
506
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
507
        $accountinggroup = $this->objFromFixture(Group::class, 'accountinggroup');
508
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
509
510
        $this->assertTrue(
511
            $staffmember->inGroup($staffgroup),
512
            'Direct group membership is detected'
513
        );
514
        $this->assertTrue(
515
            $managementmember->inGroup($staffgroup),
516
            'Users of child group are members of a direct parent group (if not in strict mode)'
517
        );
518
        $this->assertTrue(
519
            $accountingmember->inGroup($staffgroup),
520
            'Users of child group are members of a direct parent group (if not in strict mode)'
521
        );
522
        $this->assertTrue(
523
            $ceomember->inGroup($staffgroup),
524
            'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
525
        );
526
        $this->assertTrue(
527
            $ceomember->inGroup($ceogroup, true),
528
            'Direct group membership is dected (if in strict mode)'
529
        );
530
        $this->assertFalse(
531
            $ceomember->inGroup($staffgroup, true),
532
            'Users of child group are not members of a direct parent group (if in strict mode)'
533
        );
534
        $this->assertFalse(
535
            $staffmember->inGroup($managementgroup),
536
            'Users of parent group are not members of a direct child group'
537
        );
538
        $this->assertFalse(
539
            $staffmember->inGroup($ceogroup),
540
            'Users of parent group are not members of an indirect grandchild group'
541
        );
542
        $this->assertFalse(
543
            $accountingmember->inGroup($managementgroup),
544
            'Users of group are not members of any siblings'
545
        );
546
        $this->assertFalse(
547
            $staffmember->inGroup('does-not-exist'),
548
            'Non-existant group returns false'
549
        );
550
    }
551
552
    /**
553
     * Tests that the user is able to view their own record, and in turn, they can
554
     * edit and delete their own record too.
555
     */
556
    public function testCanManipulateOwnRecord()
557
    {
558
        $extensions = $this->removeExtensions(Object::get_extensions(Member::class));
559
        $member = $this->objFromFixture(Member::class, 'test');
560
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
561
562
        $this->session()->inst_set('loggedInAs', null);
563
564
        /* Not logged in, you can't view, delete or edit the record */
565
        $this->assertFalse($member->canView());
566
        $this->assertFalse($member->canDelete());
567
        $this->assertFalse($member->canEdit());
568
569
        /* Logged in users can edit their own record */
570
        $this->session()->inst_set('loggedInAs', $member->ID);
571
        $this->assertTrue($member->canView());
572
        $this->assertFalse($member->canDelete());
573
        $this->assertTrue($member->canEdit());
574
575
        /* Other uses cannot view, delete or edit others records */
576
        $this->session()->inst_set('loggedInAs', $member2->ID);
577
        $this->assertFalse($member->canView());
578
        $this->assertFalse($member->canDelete());
579
        $this->assertFalse($member->canEdit());
580
581
        $this->addExtensions($extensions);
582
        $this->session()->inst_set('loggedInAs', null);
583
    }
584
585
    public function testAuthorisedMembersCanManipulateOthersRecords()
586
    {
587
        $extensions = $this->removeExtensions(Object::get_extensions(Member::class));
588
        $member = $this->objFromFixture(Member::class, 'test');
589
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
590
591
        /* Group members with SecurityAdmin permissions can manipulate other records */
592
        $this->session()->inst_set('loggedInAs', $member->ID);
593
        $this->assertTrue($member2->canView());
594
        $this->assertTrue($member2->canDelete());
595
        $this->assertTrue($member2->canEdit());
596
597
        $this->addExtensions($extensions);
598
        $this->session()->inst_set('loggedInAs', null);
599
    }
600
601
    public function testExtendedCan()
602
    {
603
        $extensions = $this->removeExtensions(Object::get_extensions(Member::class));
604
        $member = $this->objFromFixture(Member::class, 'test');
605
606
        /* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
607
        $this->assertFalse($member->canView());
608
        $this->assertFalse($member->canDelete());
609
        $this->assertFalse($member->canEdit());
610
611
        /* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
612
        Member::add_extension(MemberTest\ViewingAllowedExtension::class);
613
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
614
615
        $this->assertTrue($member2->canView());
616
        $this->assertFalse($member2->canDelete());
617
        $this->assertFalse($member2->canEdit());
618
619
        /* Apply a extension that denies viewing of the Member */
620
        Member::remove_extension(MemberTest\ViewingAllowedExtension::class);
621
        Member::add_extension(MemberTest\ViewingDeniedExtension::class);
622
        $member3 = $this->objFromFixture(Member::class, 'managementmember');
623
624
        $this->assertFalse($member3->canView());
625
        $this->assertFalse($member3->canDelete());
626
        $this->assertFalse($member3->canEdit());
627
628
        /* Apply a extension that allows viewing and editing but denies deletion */
629
        Member::remove_extension(MemberTest\ViewingDeniedExtension::class);
630
        Member::add_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
631
        $member4 = $this->objFromFixture(Member::class, 'accountingmember');
632
633
        $this->assertTrue($member4->canView());
634
        $this->assertFalse($member4->canDelete());
635
        $this->assertTrue($member4->canEdit());
636
637
        Member::remove_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
638
        $this->addExtensions($extensions);
639
    }
640
641
    /**
642
     * Tests for {@link Member::getName()} and {@link Member::setName()}
643
     */
644
    public function testName()
645
    {
646
        $member = $this->objFromFixture(Member::class, 'test');
647
        $member->setName('Test Some User');
648
        $this->assertEquals('Test Some User', $member->getName());
649
        $member->setName('Test');
650
        $this->assertEquals('Test', $member->getName());
651
        $member->FirstName = 'Test';
0 ignored issues
show
Documentation introduced by
The property FirstName 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...
652
        $member->Surname = '';
0 ignored issues
show
Documentation introduced by
The property Surname 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...
653
        $this->assertEquals('Test', $member->getName());
654
    }
655
656
    public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves()
657
    {
658
        $adminMember = $this->objFromFixture(Member::class, 'admin');
659
        $otherAdminMember = $this->objFromFixture(Member::class, 'other-admin');
660
        $securityAdminMember = $this->objFromFixture(Member::class, 'test');
661
        $ceoMember = $this->objFromFixture(Member::class, 'ceomember');
662
663
        // Careful: Don't read as english language.
664
        // More precisely this should read canBeEditedBy()
665
666
        $this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
0 ignored issues
show
Bug introduced by
It seems like $adminMember defined by $this->objFromFixture(\S...Member::class, 'admin') on line 658 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
667
        $this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
0 ignored issues
show
Bug introduced by
It seems like $adminMember defined by $this->objFromFixture(\S...Member::class, 'admin') on line 658 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
668
        $this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
0 ignored issues
show
Bug introduced by
It seems like $adminMember defined by $this->objFromFixture(\S...Member::class, 'admin') on line 658 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
669
670
        $this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
0 ignored issues
show
Bug introduced by
It seems like $securityAdminMember defined by $this->objFromFixture(\S...\Member::class, 'test') on line 660 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
671
        $this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
0 ignored issues
show
Bug introduced by
It seems like $securityAdminMember defined by $this->objFromFixture(\S...\Member::class, 'test') on line 660 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
672
        $this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
0 ignored issues
show
Bug introduced by
It seems like $securityAdminMember defined by $this->objFromFixture(\S...\Member::class, 'test') on line 660 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
673
    }
674
675
    public function testOnChangeGroups()
676
    {
677
        $staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
678
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
679
        $adminMember = $this->objFromFixture(Member::class, 'admin');
680
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
681
        $newAdminGroup->write();
682
        Permission::grant($newAdminGroup->ID, 'ADMIN');
683
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
684
        $newOtherGroup->write();
685
686
        $this->assertTrue(
687
            $staffMember->onChangeGroups(array($staffGroup->ID)),
688
            'Adding existing non-admin group relation is allowed for non-admin members'
689
        );
690
        $this->assertTrue(
691
            $staffMember->onChangeGroups(array($newOtherGroup->ID)),
692
            'Adding new non-admin group relation is allowed for non-admin members'
693
        );
694
        $this->assertFalse(
695
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
696
            'Adding new admin group relation is not allowed for non-admin members'
697
        );
698
699
        $this->session()->inst_set('loggedInAs', $adminMember->ID);
700
        $this->assertTrue(
701
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
702
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
703
        );
704
        $this->session()->inst_set('loggedInAs', null);
705
706
        $this->assertTrue(
707
            $adminMember->onChangeGroups(array($newAdminGroup->ID)),
708
            'Adding new admin group relation is allowed for admin members'
709
        );
710
    }
711
712
    /**
713
     * Test Member_GroupSet::add
714
     */
715
    public function testOnChangeGroupsByAdd()
716
    {
717
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
718
        $adminMember = $this->objFromFixture(Member::class, 'admin');
719
720
        // Setup new admin group
721
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
722
        $newAdminGroup->write();
723
        Permission::grant($newAdminGroup->ID, 'ADMIN');
724
725
        // Setup non-admin group
726
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
727
        $newOtherGroup->write();
728
729
        // Test staff can be added to other group
730
        $this->assertFalse($staffMember->inGroup($newOtherGroup));
731
        $staffMember->Groups()->add($newOtherGroup);
732
        $this->assertTrue(
733
            $staffMember->inGroup($newOtherGroup),
734
            'Adding new non-admin group relation is allowed for non-admin members'
735
        );
736
737
        // Test staff member can't be added to admin groups
738
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
739
        $staffMember->Groups()->add($newAdminGroup);
740
        $this->assertFalse(
741
            $staffMember->inGroup($newAdminGroup),
742
            'Adding new admin group relation is not allowed for non-admin members'
743
        );
744
745
        // Test staff member can be added to admin group by admins
746
        $this->logInAs($adminMember);
747
        $staffMember->Groups()->add($newAdminGroup);
748
        $this->assertTrue(
749
            $staffMember->inGroup($newAdminGroup),
750
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
751
        );
752
753
        // Test staff member can be added if they are already admin
754
        $this->session()->inst_set('loggedInAs', null);
755
        $this->assertFalse($adminMember->inGroup($newAdminGroup));
756
        $adminMember->Groups()->add($newAdminGroup);
757
        $this->assertTrue(
758
            $adminMember->inGroup($newAdminGroup),
759
            'Adding new admin group relation is allowed for admin members'
760
        );
761
    }
762
763
    /**
764
     * Test Member_GroupSet::add
765
     */
766
    public function testOnChangeGroupsBySetIDList()
767
    {
768
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
769
770
        // Setup new admin group
771
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
772
        $newAdminGroup->write();
773
        Permission::grant($newAdminGroup->ID, 'ADMIN');
774
775
        // Test staff member can't be added to admin groups
776
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
777
        $staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
778
        $this->assertFalse(
779
            $staffMember->inGroup($newAdminGroup),
780
            'Adding new admin group relation is not allowed for non-admin members'
781
        );
782
    }
783
784
    /**
785
     * Test that extensions using updateCMSFields() are applied correctly
786
     */
787
    public function testUpdateCMSFields()
788
    {
789
        Member::add_extension(FieldsExtension::class);
790
791
        $member = Member::singleton();
792
        $fields = $member->getCMSFields();
793
794
        /**
795
 * @skipUpgrade
796
*/
797
        $this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
798
        $this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
799
        $this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
800
801
        Member::remove_extension(FieldsExtension::class);
802
    }
803
804
    /**
805
     * Test that all members are returned
806
     */
807
    public function testMap_in_groupsReturnsAll()
808
    {
809
        $members = Member::map_in_groups();
810
        $this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
811
    }
812
813
    /**
814
     * Test that only admin members are returned
815
     */
816
    public function testMap_in_groupsReturnsAdmins()
817
    {
818
        $adminID = $this->objFromFixture(Group::class, 'admingroup')->ID;
819
        $members = Member::map_in_groups($adminID)->toArray();
820
821
        $admin = $this->objFromFixture(Member::class, 'admin');
822
        $otherAdmin = $this->objFromFixture(Member::class, 'other-admin');
823
824
        $this->assertTrue(
825
            in_array($admin->getTitle(), $members),
826
            $admin->getTitle().' should be in the returned list.'
827
        );
828
        $this->assertTrue(
829
            in_array($otherAdmin->getTitle(), $members),
830
            $otherAdmin->getTitle().' should be in the returned list.'
831
        );
832
        $this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
833
    }
834
835
    /**
836
     * Add the given array of member extensions as class names.
837
     * This is useful for re-adding extensions after being removed
838
     * in a test case to produce an unbiased test.
839
     *
840
     * @param  array $extensions
841
     * @return array The added extensions
842
     */
843
    protected function addExtensions($extensions)
844
    {
845
        if ($extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions 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...
846
            foreach ($extensions as $extension) {
847
                Member::add_extension($extension);
848
            }
849
        }
850
        return $extensions;
851
    }
852
853
    /**
854
     * Remove given extensions from Member. This is useful for
855
     * removing extensions that could produce a biased
856
     * test result, as some extensions applied by project
857
     * code or modules can do this.
858
     *
859
     * @param  array $extensions
860
     * @return array The removed extensions
861
     */
862
    protected function removeExtensions($extensions)
863
    {
864
        if ($extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions 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...
865
            foreach ($extensions as $extension) {
866
                Member::remove_extension($extension);
867
            }
868
        }
869
        return $extensions;
870
    }
871
872
    public function testGenerateAutologinTokenAndStoreHash()
873
    {
874
        $enc = new PasswordEncryptor_Blowfish();
875
876
        $m = new Member();
877
        $m->PasswordEncryption = 'blowfish';
878
        $m->Salt = $enc->salt('123');
879
880
        $token = $m->generateAutologinTokenAndStoreHash();
881
882
        $this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as ahash.');
883
    }
884
885
    public function testValidateAutoLoginToken()
886
    {
887
        $enc = new PasswordEncryptor_Blowfish();
888
889
        $m1 = new Member();
890
        $m1->PasswordEncryption = 'blowfish';
891
        $m1->Salt = $enc->salt('123');
892
        $m1Token = $m1->generateAutologinTokenAndStoreHash();
893
894
        $m2 = new Member();
895
        $m2->PasswordEncryption = 'blowfish';
896
        $m2->Salt = $enc->salt('456');
897
        $m2Token = $m2->generateAutologinTokenAndStoreHash();
898
899
        $this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
900
        $this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
901
    }
902
903
    public function testRememberMeHashGeneration()
904
    {
905
        $m1 = $this->objFromFixture(Member::class, 'grouplessmember');
906
907
        $m1->login(true);
908
        $hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
909
        $this->assertEquals($hashes->count(), 1);
910
        $firstHash = $hashes->first();
911
        $this->assertNotNull($firstHash->DeviceID);
912
        $this->assertNotNull($firstHash->Hash);
913
    }
914
915
    public function testRememberMeHashAutologin()
916
    {
917
        /**
918
 * @var Member $m1
919
*/
920
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
921
922
        $m1->logIn(true);
923
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
924
        $this->assertNotNull($firstHash);
925
926
        // re-generates the hash so we can get the token
927
        $firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash 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...
928
        $token = $firstHash->getToken();
929
        $firstHash->write();
930
931
        $response = $this->get(
932
            'Security/login',
933
            $this->session(),
934
            null,
935
            array(
936
                'alc_enc' => $m1->ID.':'.$token,
937
                'alc_device' => $firstHash->DeviceID
938
            )
939
        );
940
        $message = Convert::raw2xml(
941
            _t(
942
                'Member.LOGGEDINAS',
943
                "You're logged in as {name}.",
944
                array('name' => $m1->FirstName)
945
            )
946
        );
947
        $this->assertContains($message, $response->getBody());
948
949
        $this->session()->inst_set('loggedInAs', null);
950
951
        // A wrong token or a wrong device ID should not let us autologin
952
        $response = $this->get(
953
            'Security/login',
954
            $this->session(),
955
            null,
956
            array(
957
                'alc_enc' => $m1->ID.':'.str_rot13($token),
958
                'alc_device' => $firstHash->DeviceID
959
            )
960
        );
961
        $this->assertNotContains($message, $response->getBody());
962
963
        $response = $this->get(
964
            'Security/login',
965
            $this->session(),
966
            null,
967
            array(
968
                'alc_enc' => $m1->ID.':'.$token,
969
                'alc_device' => str_rot13($firstHash->DeviceID)
970
            )
971
        );
972
        $this->assertNotContains($message, $response->getBody());
973
974
        // Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
975
        // should remove all previous hashes for this device
976
        $response = $this->post(
977
            'Security/LoginForm',
978
            array(
979
                'Email' => $m1->Email,
980
                'Password' => '1nitialPassword',
981
                'AuthenticationMethod' => MemberAuthenticator::class,
982
                'action_dologin' => 'action_dologin'
983
            ),
984
            null,
985
            $this->session(),
986
            null,
987
            array(
988
                'alc_device' => $firstHash->DeviceID
989
            )
990
        );
991
        $this->assertContains($message, $response->getBody());
992
        $this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), 0);
993
    }
994
995
    public function testExpiredRememberMeHashAutologin()
996
    {
997
        /**
998
 * @var Member $m1
999
*/
1000
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1001
        $m1->logIn(true);
1002
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1003
        $this->assertNotNull($firstHash);
1004
1005
        // re-generates the hash so we can get the token
1006
        $firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash 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...
1007
        $token = $firstHash->getToken();
1008
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
0 ignored issues
show
Documentation introduced by
The property ExpiryDate 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...
1009
        $firstHash->write();
1010
1011
        DBDatetime::set_mock_now('1999-12-31 23:59:59');
1012
1013
        $response = $this->get(
1014
            'Security/login',
1015
            $this->session(),
1016
            null,
1017
            array(
1018
                'alc_enc' => $m1->ID.':'.$token,
1019
                'alc_device' => $firstHash->DeviceID
1020
            )
1021
        );
1022
        $message = Convert::raw2xml(
1023
            _t(
1024
                'Member.LOGGEDINAS',
1025
                "You're logged in as {name}.",
1026
                array('name' => $m1->FirstName)
1027
            )
1028
        );
1029
        $this->assertContains($message, $response->getBody());
1030
1031
        $this->session()->inst_set('loggedInAs', null);
1032
1033
        // re-generates the hash so we can get the token
1034
        $firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash 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...
1035
        $token = $firstHash->getToken();
1036
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
0 ignored issues
show
Documentation introduced by
The property ExpiryDate 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...
1037
        $firstHash->write();
1038
1039
        DBDatetime::set_mock_now('2000-01-01 00:00:01');
1040
1041
        $response = $this->get(
1042
            'Security/login',
1043
            $this->session(),
1044
            null,
1045
            array(
1046
                'alc_enc' => $m1->ID.':'.$token,
1047
                'alc_device' => $firstHash->DeviceID
1048
            )
1049
        );
1050
        $this->assertNotContains($message, $response->getBody());
1051
        $this->session()->inst_set('loggedInAs', null);
1052
        DBDatetime::clear_mock_now();
1053
    }
1054
1055
    public function testRememberMeMultipleDevices()
1056
    {
1057
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1058
1059
        // First device
1060
        $m1->login(true);
1061
        Cookie::set('alc_device', null);
1062
        // Second device
1063
        $m1->login(true);
1064
1065
        // Hash of first device
1066
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1067
        $this->assertNotNull($firstHash);
1068
1069
        // Hash of second device
1070
        $secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last();
1071
        $this->assertNotNull($secondHash);
1072
1073
        // DeviceIDs are different
1074
        $this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
1075
1076
        // re-generates the hashes so we can get the tokens
1077
        $firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash 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...
1078
        $firstToken = $firstHash->getToken();
1079
        $firstHash->write();
1080
1081
        $secondHash->Hash = $secondHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash 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...
1082
        $secondToken = $secondHash->getToken();
1083
        $secondHash->write();
1084
1085
        // Accessing the login page should show the user's name straight away
1086
        $response = $this->get(
1087
            'Security/login',
1088
            $this->session(),
1089
            null,
1090
            array(
1091
                'alc_enc' => $m1->ID.':'.$firstToken,
1092
                'alc_device' => $firstHash->DeviceID
1093
            )
1094
        );
1095
        $message = Convert::raw2xml(
1096
            _t(
1097
                'Member.LOGGEDINAS',
1098
                "You're logged in as {name}.",
1099
                array('name' => $m1->FirstName)
1100
            )
1101
        );
1102
        $this->assertContains($message, $response->getBody());
1103
1104
        $this->session()->inst_set('loggedInAs', null);
1105
1106
        // Accessing the login page from the second device
1107
        $response = $this->get(
1108
            'Security/login',
1109
            $this->session(),
1110
            null,
1111
            array(
1112
                'alc_enc' => $m1->ID.':'.$secondToken,
1113
                'alc_device' => $secondHash->DeviceID
1114
            )
1115
        );
1116
        $this->assertContains($message, $response->getBody());
1117
1118
        // Logging out from the second device - only one device being logged out
1119
        RememberLoginHash::config()->update('logout_across_devices', false);
1120
        $response = $this->get(
1121
            'Security/logout',
1122
            $this->session(),
1123
            null,
1124
            array(
1125
                'alc_enc' => $m1->ID.':'.$secondToken,
1126
                'alc_device' => $secondHash->DeviceID
1127
            )
1128
        );
1129
        $this->assertEquals(
1130
            RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->count(),
1131
            1
1132
        );
1133
1134
        // Logging out from any device when all login hashes should be removed
1135
        RememberLoginHash::config()->update('logout_across_devices', true);
1136
        $m1->login(true);
1137
        $response = $this->get('Security/logout', $this->session());
1138
        $this->assertEquals(
1139
            RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
1140
            0
1141
        );
1142
    }
1143
1144
    public function testCanDelete()
1145
    {
1146
        $admin1 = $this->objFromFixture(Member::class, 'admin');
1147
        $admin2 = $this->objFromFixture(Member::class, 'other-admin');
1148
        $member1 = $this->objFromFixture(Member::class, 'grouplessmember');
1149
        $member2 = $this->objFromFixture(Member::class, 'noformatmember');
1150
1151
        $this->assertTrue(
1152
            $admin1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture(\S...::class, 'other-admin') on line 1147 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1153
            'Admins can delete other admins'
1154
        );
1155
        $this->assertTrue(
1156
            $member1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture(\S...::class, 'other-admin') on line 1147 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1157
            'Admins can delete non-admins'
1158
        );
1159
        $this->assertFalse(
1160
            $admin1->canDelete($admin1),
0 ignored issues
show
Bug introduced by
It seems like $admin1 defined by $this->objFromFixture(\S...Member::class, 'admin') on line 1146 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1161
            'Admins can not delete themselves'
1162
        );
1163
        $this->assertFalse(
1164
            $member1->canDelete($member2),
0 ignored issues
show
Bug introduced by
It seems like $member2 defined by $this->objFromFixture(\S...lass, 'noformatmember') on line 1149 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1165
            'Non-admins can not delete other non-admins'
1166
        );
1167
        $this->assertFalse(
1168
            $member1->canDelete($member1),
0 ignored issues
show
Bug introduced by
It seems like $member1 defined by $this->objFromFixture(\S...ass, 'grouplessmember') on line 1148 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1169
            'Non-admins can not delete themselves'
1170
        );
1171
    }
1172
1173
    public function testFailedLoginCount()
1174
    {
1175
        $maxFailedLoginsAllowed = 3;
1176
        //set up the config variables to enable login lockouts
1177
        Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
1178
1179
        $member = $this->objFromFixture(Member::class, 'test');
1180
        $failedLoginCount = $member->FailedLoginCount;
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...
1181
1182
        for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
1183
            $member->registerFailedLogin();
1184
1185
            $this->assertEquals(
1186
                ++$failedLoginCount,
1187
                $member->FailedLoginCount,
1188
                'Failed to increment $member->FailedLoginCount'
1189
            );
1190
1191
            $this->assertFalse(
1192
                $member->isLockedOut(),
1193
                "Member has been locked out too early"
1194
            );
1195
        }
1196
    }
1197
1198
    public function testMemberValidator()
1199
    {
1200
        // clear custom requirements for this test
1201
        Member_Validator::config()->update('customRequired', null);
1202
        $memberA = $this->objFromFixture(Member::class, 'admin');
1203
        $memberB = $this->objFromFixture(Member::class, 'test');
1204
1205
        // create a blank form
1206
        $form = new MemberTest\ValidatorForm();
1207
1208
        $validator = new Member_Validator();
1209
        $validator->setForm($form);
1210
1211
        // Simulate creation of a new member via form, but use an existing member identifier
1212
        $fail = $validator->php(
1213
            array(
1214
            'FirstName' => 'Test',
1215
            'Email' => $memberA->Email
1216
            )
1217
        );
1218
1219
        $this->assertFalse(
1220
            $fail,
1221
            'Member_Validator must fail when trying to create new Member with existing Email.'
1222
        );
1223
1224
        // populate the form with values from another member
1225
        $form->loadDataFrom($memberB);
0 ignored issues
show
Bug introduced by
It seems like $memberB defined by $this->objFromFixture(\S...\Member::class, 'test') on line 1203 can be null; however, SilverStripe\Forms\Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1226
1227
        // Assign the validator to an existing member
1228
        // (this is basically the same as passing the member ID with the form data)
1229
        $validator->setForMember($memberB);
1230
1231
        // Simulate update of a member via form and use an existing member Email
1232
        $fail = $validator->php(
1233
            array(
1234
            'FirstName' => 'Test',
1235
            'Email' => $memberA->Email
1236
            )
1237
        );
1238
1239
        // Simulate update to a new Email address
1240
        $pass1 = $validator->php(
1241
            array(
1242
            'FirstName' => 'Test',
1243
            'Email' => '[email protected]'
1244
            )
1245
        );
1246
1247
        // Pass in the same Email address that the member already has. Ensure that case is valid
1248
        $pass2 = $validator->php(
1249
            array(
1250
            'FirstName' => 'Test',
1251
            'Surname' => 'User',
1252
            'Email' => $memberB->Email
1253
            )
1254
        );
1255
1256
        $this->assertFalse(
1257
            $fail,
1258
            'Member_Validator must fail when trying to update existing member with existing Email.'
1259
        );
1260
1261
        $this->assertTrue(
1262
            $pass1,
1263
            'Member_Validator must pass when Email is updated to a value that\'s not in use.'
1264
        );
1265
1266
        $this->assertTrue(
1267
            $pass2,
1268
            'Member_Validator must pass when Member updates his own Email to the already existing value.'
1269
        );
1270
    }
1271
1272
    public function testMemberValidatorWithExtensions()
1273
    {
1274
        // clear custom requirements for this test
1275
        Member_Validator::config()->update('customRequired', null);
1276
1277
        // create a blank form
1278
        $form = new MemberTest\ValidatorForm();
1279
1280
        // Test extensions
1281
        Member_Validator::add_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1282
        $validator = new Member_Validator();
1283
        $validator->setForm($form);
1284
1285
        // This test should fail, since the extension enforces FirstName == Surname
1286
        $fail = $validator->php(
1287
            array(
1288
            'FirstName' => 'Test',
1289
            'Surname' => 'User',
1290
            'Email' => '[email protected]'
1291
            )
1292
        );
1293
1294
        $pass = $validator->php(
1295
            array(
1296
            'FirstName' => 'Test',
1297
            'Surname' => 'Test',
1298
            'Email' => '[email protected]'
1299
            )
1300
        );
1301
1302
        $this->assertFalse(
1303
            $fail,
1304
            'Member_Validator must fail because of added extension.'
1305
        );
1306
1307
        $this->assertTrue(
1308
            $pass,
1309
            'Member_Validator must succeed, since it meets all requirements.'
1310
        );
1311
1312
        // Add another extension that always fails. This ensures that all extensions are considered in the validation
1313
        Member_Validator::add_extension(MemberTest\AlwaysFailExtension::class);
1314
        $validator = new Member_Validator();
1315
        $validator->setForm($form);
1316
1317
        // Even though the data is valid, This test should still fail, since one extension always returns false
1318
        $fail = $validator->php(
1319
            array(
1320
            'FirstName' => 'Test',
1321
            'Surname' => 'Test',
1322
            'Email' => '[email protected]'
1323
            )
1324
        );
1325
1326
        $this->assertFalse(
1327
            $fail,
1328
            'Member_Validator must fail because of added extensions.'
1329
        );
1330
1331
        // Remove added extensions
1332
        Member_Validator::remove_extension(MemberTest\AlwaysFailExtension::class);
1333
        Member_Validator::remove_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1334
    }
1335
1336
    public function testCustomMemberValidator()
1337
    {
1338
        // clear custom requirements for this test
1339
        Member_Validator::config()->update('customRequired', null);
1340
1341
        $member = $this->objFromFixture(Member::class, 'admin');
1342
1343
        $form = new MemberTest\ValidatorForm();
1344
        $form->loadDataFrom($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture(\S...Member::class, 'admin') on line 1341 can be null; however, SilverStripe\Forms\Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1345
1346
        $validator = new Member_Validator();
1347
        $validator->setForm($form);
1348
1349
        $pass = $validator->php(
1350
            array(
1351
            'FirstName' => 'Borris',
1352
            'Email' => '[email protected]'
1353
            )
1354
        );
1355
1356
        $fail = $validator->php(
1357
            array(
1358
            'Email' => '[email protected]',
1359
            'Surname' => ''
1360
            )
1361
        );
1362
1363
        $this->assertTrue($pass, 'Validator requires a FirstName and Email');
1364
        $this->assertFalse($fail, 'Missing FirstName');
1365
1366
        $ext = new MemberTest\ValidatorExtension();
1367
        $ext->updateValidator($validator);
1368
1369
        $pass = $validator->php(
1370
            array(
1371
            'FirstName' => 'Borris',
1372
            'Email' => '[email protected]'
1373
            )
1374
        );
1375
1376
        $fail = $validator->php(
1377
            array(
1378
            'Email' => '[email protected]'
1379
            )
1380
        );
1381
1382
        $this->assertFalse($pass, 'Missing surname');
1383
        $this->assertFalse($fail, 'Missing surname value');
1384
1385
        $fail = $validator->php(
1386
            array(
1387
            'Email' => '[email protected]',
1388
            'Surname' => 'Silverman'
1389
            )
1390
        );
1391
1392
        $this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1393
    }
1394
1395
    public function testCurrentUser()
1396
    {
1397
        $this->assertNull(Member::currentUser());
1398
1399
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1400
        $this->logInAs($adminMember);
1401
1402
        $userFromSession = Member::currentUser();
1403
        $this->assertEquals($adminMember->ID, $userFromSession->ID);
1404
    }
1405
1406
    /**
1407
     * @covers \SilverStripe\Security\Member::actAs()
1408
     */
1409
    public function testActAsUserPermissions()
1410
    {
1411
        $this->assertNull(Member::currentUser());
1412
1413
        /** @var Member $adminMember */
1414
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1415
1416
        // Check acting as admin when not logged in
1417
        $checkAdmin = Member::actAs($adminMember, function () {
1418
            return Permission::check('ADMIN');
1419
        });
1420
        $this->assertTrue($checkAdmin);
1421
1422
        // Check nesting
1423
        $checkAdmin = Member::actAs($adminMember, function () {
1424
            return Member::actAs(null, function () {
1425
                return Permission::check('ADMIN');
1426
            });
1427
        });
1428
        $this->assertFalse($checkAdmin);
1429
1430
        // Check logging in as non-admin user
1431
        $this->logInWithPermission('TEST_PERMISSION');
1432
1433
        $hasPerm = Member::actAs(null, function () {
1434
            return Permission::check('TEST_PERMISSION');
1435
        });
1436
        $this->assertFalse($hasPerm);
1437
1438
        // Check permissions can be promoted
1439
        $checkAdmin = Member::actAs($adminMember, function () {
1440
            return Permission::check('ADMIN');
1441
        });
1442
        $this->assertTrue($checkAdmin);
1443
    }
1444
1445
    /**
1446
     * @covers \SilverStripe\Security\Member::actAs()
1447
     */
1448
    public function testActAsUser()
1449
    {
1450
        $this->assertNull(Member::currentUser());
1451
1452
        /** @var Member $adminMember */
1453
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1454
        $memberID = Member::actAs($adminMember, function () {
1455
            return Member::currentUserID();
1456
        });
1457
        $this->assertEquals($adminMember->ID, $memberID);
1458
1459
        // Check nesting
1460
        $memberID = Member::actAs($adminMember, function () {
1461
            return Member::actAs(null, function () {
1462
                return Member::currentUserID();
1463
            });
1464
        });
1465
        $this->assertEmpty($memberID);
1466
    }
1467
}
1468