Completed
Push — master ( 38a63a...1be2e7 )
by Daniel
10:36
created

MemberTest   F

Complexity

Total Complexity 51

Size/Duplication

Total Lines 1280
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 24

Importance

Changes 0
Metric Value
dl 0
loc 1280
rs 2.0617
c 0
b 0
f 0
wmc 51
lcom 2
cbo 24

46 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A __destruct() 0 3 1
A setUp() 0 7 1
A tearDown() 0 4 1
A testWriteDoesntMergeNewRecordWithExistingMember() 0 9 1
A testWriteDoesntMergeExistingMemberOnIdentifierChange() 0 12 1
A testDefaultPasswordEncryptionOnMember() 0 17 1
A testDefaultPasswordEncryptionDoesntChangeExistingMembers() 0 21 1
A testKeepsEncryptionOnEmptyPasswords() 0 16 1
A testSetPassword() 0 7 1
B testPasswordChangeLogging() 0 36 1
A testChangedPasswordEmaling() 0 14 1
A testForgotPasswordEmaling() 0 20 1
B testValidatePassword() 0 78 1
A testPasswordExpirySetting() 0 17 1
A testIsPasswordExpired() 0 22 1
A testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() 0 8 1
B testInGroups() 0 24 1
A testAddToGroupByCode() 0 23 1
B testRemoveFromGroupByCode() 0 27 1
A testInGroup() 0 52 1
B testCanManipulateOwnRecord() 0 27 1
A testAuthorisedMembersCanManipulateOthersRecords() 0 14 1
B testExtendedCan() 0 38 1
A testName() 0 10 1
A testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves() 0 17 1
B testOnChangeGroups() 0 35 1
B testOnChangeGroupsByAdd() 0 46 1
A testOnChangeGroupsBySetIDList() 0 16 1
A testUpdateCMSFields() 0 13 1
A testMap_in_groupsReturnsAll() 0 4 1
A testMap_in_groupsReturnsAdmins() 0 13 1
A addExtensions() 0 6 3
A removeExtensions() 0 6 3
A testGenerateAutologinTokenAndStoreHash() 0 11 1
A testValidateAutoLoginToken() 0 16 1
A testRememberMeHashGeneration() 0 10 1
A testRememberMeHashAutologin() 0 74 1
A testExpiredRememberMeHashAutologin() 0 54 1
B testRememberMeMultipleDevices() 0 85 1
B testCanDelete() 0 27 1
A testFailedLoginCount() 0 23 2
A testMemberValidator() 0 65 1
A testMemberValidatorWithExtensions() 0 57 1
A testCustomMemberValidator() 0 48 1
A testCurrentUser() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like MemberTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MemberTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Core\Object;
6
use SilverStripe\Dev\FunctionalTest;
7
use SilverStripe\Control\Cookie;
8
use SilverStripe\i18n\i18n;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\DB;
11
use SilverStripe\ORM\FieldType\DBDatetime;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\MemberAuthenticator;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Security\MemberPassword;
16
use SilverStripe\Security\Group;
17
use SilverStripe\Security\Permission;
18
use SilverStripe\Security\PasswordEncryptor_Blowfish;
19
use SilverStripe\Security\RememberLoginHash;
20
use SilverStripe\Security\Member_Validator;
21
use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
22
23
class MemberTest extends FunctionalTest {
24
	protected static $fixture_file = 'MemberTest.yml';
25
26
	protected $orig = array();
27
	protected $local = null;
28
29
	protected $illegalExtensions = array(
30
		Member::class => array(
31
			// TODO Coupling with modules, this should be resolved by automatically
32
			// removing all applied extensions before a unit test
33
			'ForumRole',
34
			'OpenIDAuthenticatedRole'
35
		)
36
	);
37
38
	public function __construct() {
39
		parent::__construct();
40
41
		//Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
42
		//This is because the test relies on the yaml file being interpreted according to a particular date format
43
		//and this setup occurs before the setUp method is run
44
		$this->local = i18n::config()->get('default_locale');
45
		i18n::config()->update('default_locale', 'en_US');
46
	}
47
48
	public function __destruct() {
49
		i18n::config()->update('default_locale', $this->local);
50
	}
51
52
	/**
53
	 * @skipUpgrade
54
	 */
55
	public function setUp() {
56
		parent::setUp();
57
58
		$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...
59
		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...
60
		Member::set_password_validator(null);
61
	}
62
63
	public function tearDown() {
64
		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...
65
		parent::tearDown();
66
	}
67
68
69
70
	/**
71
	 * @expectedException \SilverStripe\ORM\ValidationException
72
	 */
73
	public function testWriteDoesntMergeNewRecordWithExistingMember() {
74
		$m1 = new Member();
75
		$m1->Email = '[email protected]';
76
		$m1->write();
77
78
		$m2 = new Member();
79
		$m2->Email = '[email protected]';
80
		$m2->write();
81
	}
82
83
	/**
84
	 * @expectedException \SilverStripe\ORM\ValidationException
85
	 */
86
	public function testWriteDoesntMergeExistingMemberOnIdentifierChange() {
87
		$m1 = new Member();
88
		$m1->Email = '[email protected]';
89
		$m1->write();
90
91
		$m2 = new Member();
92
		$m2->Email = '[email protected]';
93
		$m2->write();
94
95
		$m2->Email = '[email protected]';
96
		$m2->write();
97
	}
98
99
	public function testDefaultPasswordEncryptionOnMember() {
100
		$memberWithPassword = new Member();
101
		$memberWithPassword->Password = 'mypassword';
102
		$memberWithPassword->write();
103
		$this->assertEquals(
104
			$memberWithPassword->PasswordEncryption,
105
			Security::config()->password_encryption_algorithm,
106
			'Password encryption is set for new member records on first write (with setting "Password")'
107
		);
108
109
		$memberNoPassword = new Member();
110
		$memberNoPassword->write();
111
		$this->assertNull(
112
			$memberNoPassword->PasswordEncryption,
113
			'Password encryption is not set for new member records on first write, when not setting a "Password")'
114
		);
115
	}
116
117
	public function testDefaultPasswordEncryptionDoesntChangeExistingMembers() {
118
		$member = new Member();
119
		$member->Password = 'mypassword';
120
		$member->PasswordEncryption = 'sha1_v2.4';
121
		$member->write();
122
123
		$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...
124
		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...
125
126
		$member->Password = 'mynewpassword';
127
		$member->write();
128
129
		$this->assertEquals(
130
			$member->PasswordEncryption,
131
			'sha1_v2.4'
132
		);
133
		$result = $member->checkPassword('mynewpassword');
134
		$this->assertTrue($result->valid());
135
136
		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...
137
	}
138
139
	public function testKeepsEncryptionOnEmptyPasswords() {
140
		$member = new Member();
141
		$member->Password = 'mypassword';
142
		$member->PasswordEncryption = 'sha1_v2.4';
143
		$member->write();
144
145
		$member->Password = '';
146
		$member->write();
147
148
		$this->assertEquals(
149
			$member->PasswordEncryption,
150
			'sha1_v2.4'
151
		);
152
		$result = $member->checkPassword('');
153
		$this->assertTrue($result->valid());
154
	}
155
156
	public function testSetPassword() {
157
		$member = $this->objFromFixture(Member::class, 'test');
158
		$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...
159
		$member->write();
160
		$result = $member->checkPassword('test1');
161
		$this->assertTrue($result->valid());
162
	}
163
164
	/**
165
	 * Test that password changes are logged properly
166
	 */
167
	public function testPasswordChangeLogging() {
168
		$member = $this->objFromFixture(Member::class, 'test');
169
		$this->assertNotNull($member);
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
173
		$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...
174
		$member->write();
175
176
		$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...
177
		$member->write();
178
179
		$passwords = DataObject::get("SilverStripe\\Security\\MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
180
			->getIterator();
181
		$this->assertNotNull($passwords);
182
		$passwords->rewind();
183
		$this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
184
185
		$passwords->next();
186
		$this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
187
188
		$passwords->next();
189
		$this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
190
191
		$passwords->next();
192
		$this->assertInstanceOf('SilverStripe\\ORM\\DataObject', $passwords->current());
193
		$this->assertTrue($passwords->current()->checkPassword('1nitialPassword'),
194
			"Password 1nitialPassword not found in MemberRecord");
195
196
		//check we don't retain orphaned records when a member is deleted
197
		$member->delete();
198
199
		$passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
200
201
		$this->assertCount(0, $passwords);
202
	}
203
204
	/**
205
	 * Test that changed passwords will send an email
206
	 */
207
	public function testChangedPasswordEmaling() {
208
		Member::config()->update('notify_password_change', true);
209
210
		$this->clearEmails();
211
212
		$member = $this->objFromFixture(Member::class, 'test');
213
		$this->assertNotNull($member);
214
		$valid = $member->changePassword('32asDF##$$%%');
215
		$this->assertTrue($valid->valid());
216
217
		$this->assertEmailSent('[email protected]', null, 'Your password has been changed',
218
			'/testuser@example\.com/');
219
220
	}
221
222
	/**
223
	 * Test that triggering "forgotPassword" sends an Email with a reset link
224
		*/
225
	public function testForgotPasswordEmaling() {
226
		$this->clearEmails();
227
		$this->autoFollowRedirection = false;
228
229
		$member = $this->objFromFixture(Member::class, 'test');
230
		$this->assertNotNull($member);
231
232
		// Initiate a password-reset
233
		$response = $this->post('Security/LostPasswordForm', array('Email' => $member->Email));
234
235
		$this->assertEquals($response->getStatusCode(), 302);
236
237
		// We should get redirected to Security/passwordsent
238
		$this->assertContains('Security/passwordsent/[email protected]',
239
			urldecode($response->getHeader('Location')));
240
241
		// Check existance of reset link
242
		$this->assertEmailSent("[email protected]", null, 'Your password reset link',
243
			'/Security\/changepassword\?m='.$member->ID.'&amp;t=[^"]+/');
244
	}
245
246
	/**
247
	 * Test that passwords validate against NZ e-government guidelines
248
	 *  - don't allow the use of the last 6 passwords
249
	 *  - require at least 3 of lowercase, uppercase, digits and punctuation
250
	 *  - at least 7 characters long
251
	 */
252
	public function testValidatePassword() {
253
		$member = $this->objFromFixture(Member::class, 'test');
254
		$this->assertNotNull($member);
255
256
		Member::set_password_validator(new MemberTest\TestPasswordValidator());
257
258
		// BAD PASSWORDS
259
260
		$valid = $member->changePassword('shorty');
261
		$this->assertFalse($valid->valid());
262
		$this->assertContains("TOO_SHORT", $valid->codeList());
263
264
		$valid = $member->changePassword('longone');
265
		$this->assertNotContains("TOO_SHORT", $valid->codeList());
266
		$this->assertContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
267
		$this->assertFalse($valid->valid());
268
269
		$valid = $member->changePassword('w1thNumb3rs');
270
		$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
271
		$this->assertTrue($valid->valid());
272
273
		// Clear out the MemberPassword table to ensure that the system functions properly in that situation
274
		DB::query("DELETE FROM \"MemberPassword\"");
275
276
		// GOOD PASSWORDS
277
278
		$valid = $member->changePassword('withSym###Ls');
279
		$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
280
		$this->assertTrue($valid->valid());
281
282
		$valid = $member->changePassword('withSym###Ls2');
283
		$this->assertTrue($valid->valid());
284
285
		$valid = $member->changePassword('withSym###Ls3');
286
		$this->assertTrue($valid->valid());
287
288
		$valid = $member->changePassword('withSym###Ls4');
289
		$this->assertTrue($valid->valid());
290
291
		$valid = $member->changePassword('withSym###Ls5');
292
		$this->assertTrue($valid->valid());
293
294
		$valid = $member->changePassword('withSym###Ls6');
295
		$this->assertTrue($valid->valid());
296
297
		$valid = $member->changePassword('withSym###Ls7');
298
		$this->assertTrue($valid->valid());
299
300
		// CAN'T USE PASSWORDS 2-7, but I can use pasword 1
301
302
		$valid = $member->changePassword('withSym###Ls2');
303
		$this->assertFalse($valid->valid());
304
		$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
305
306
		$valid = $member->changePassword('withSym###Ls5');
307
		$this->assertFalse($valid->valid());
308
		$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
309
310
		$valid = $member->changePassword('withSym###Ls7');
311
		$this->assertFalse($valid->valid());
312
		$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
313
314
		$valid = $member->changePassword('withSym###Ls');
315
		$this->assertTrue($valid->valid());
316
317
		// HAVING DONE THAT, PASSWORD 2 is now available from the list
318
319
		$valid = $member->changePassword('withSym###Ls2');
320
		$this->assertTrue($valid->valid());
321
322
		$valid = $member->changePassword('withSym###Ls3');
323
		$this->assertTrue($valid->valid());
324
325
		$valid = $member->changePassword('withSym###Ls4');
326
		$this->assertTrue($valid->valid());
327
328
		Member::set_password_validator(null);
329
	}
330
331
	/**
332
	 * Test that the PasswordExpiry date is set when passwords are changed
333
	 */
334
	public function testPasswordExpirySetting() {
335
		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...
336
337
		$member = $this->objFromFixture(Member::class, 'test');
338
		$this->assertNotNull($member);
339
		$valid = $member->changePassword("Xx?1234234");
340
		$this->assertTrue($valid->valid());
341
342
		$expiryDate = date('Y-m-d', time() + 90*86400);
343
		$this->assertEquals($expiryDate, $member->PasswordExpiry);
344
345
		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...
346
		$valid = $member->changePassword("Xx?1234235");
347
		$this->assertTrue($valid->valid());
348
349
		$this->assertNull($member->PasswordExpiry);
350
	}
351
352
	public function testIsPasswordExpired() {
353
		$member = $this->objFromFixture(Member::class, 'test');
354
		$this->assertNotNull($member);
355
		$this->assertFalse($member->isPasswordExpired());
356
357
		$member = $this->objFromFixture(Member::class, 'noexpiry');
358
		$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...
359
		$this->assertFalse($member->isPasswordExpired());
360
361
		$member = $this->objFromFixture(Member::class, 'expiredpassword');
362
		$this->assertTrue($member->isPasswordExpired());
363
364
		// Check the boundary conditions
365
		// If PasswordExpiry == today, then it's expired
366
		$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...
367
		$this->assertTrue($member->isPasswordExpired());
368
369
		// If PasswordExpiry == tomorrow, then it's not
370
		$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...
371
		$this->assertFalse($member->isPasswordExpired());
372
373
	}
374
375
	public function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() {
376
		i18n::config()
377
			->update('date_format', 'yyyy-MM-dd')
378
			->update('time_format', 'H:mm');
379
		$member = $this->objFromFixture(Member::class, 'noformatmember');
380
		$this->assertEquals('yyyy-MM-dd', $member->DateFormat);
381
		$this->assertEquals('H:mm', $member->TimeFormat);
382
	}
383
384
	public function testInGroups() {
385
		$staffmember = $this->objFromFixture(Member::class, 'staffmember');
386
		$managementmember = $this->objFromFixture(Member::class, 'managementmember');
387
		$accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
388
		$ceomember = $this->objFromFixture(Member::class, 'ceomember');
389
390
		$staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
391
		$managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
392
		$accountinggroup = $this->objFromFixture(Group::class, 'accountinggroup');
393
		$ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
394
395
		$this->assertTrue(
396
			$staffmember->inGroups(array($staffgroup, $managementgroup)),
397
			'inGroups() succeeds if a membership is detected on one of many passed groups'
398
		);
399
		$this->assertFalse(
400
			$staffmember->inGroups(array($ceogroup, $managementgroup)),
401
			'inGroups() fails if a membership is detected on none of the passed groups'
402
		);
403
		$this->assertFalse(
404
			$ceomember->inGroups(array($staffgroup, $managementgroup), true),
405
			'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
406
		);
407
	}
408
409
	public function testAddToGroupByCode() {
410
		$grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
411
		$memberlessGroup = $this->objFromFixture(Group::class,'memberlessgroup');
412
413
		$this->assertFalse($grouplessMember->Groups()->exists());
414
		$this->assertFalse($memberlessGroup->Members()->exists());
415
416
		$grouplessMember->addToGroupByCode('memberless');
417
418
		$this->assertEquals($memberlessGroup->Members()->count(), 1);
419
		$this->assertEquals($grouplessMember->Groups()->count(), 1);
420
421
		$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
422
		$this->assertEquals($grouplessMember->Groups()->count(), 2);
423
424
		$group = DataObject::get_one(Group::class, array(
425
			'"Group"."Code"' => 'somegroupthatwouldneverexist'
426
		));
427
		$this->assertNotNull($group);
428
		$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
429
		$this->assertEquals($group->Title, 'New Group');
430
431
	}
432
433
	public function testRemoveFromGroupByCode() {
434
		$grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
435
		$memberlessGroup = $this->objFromFixture(Group::class,'memberlessgroup');
436
437
		$this->assertFalse($grouplessMember->Groups()->exists());
438
		$this->assertFalse($memberlessGroup->Members()->exists());
439
440
		$grouplessMember->addToGroupByCode('memberless');
441
442
		$this->assertEquals($memberlessGroup->Members()->count(), 1);
443
		$this->assertEquals($grouplessMember->Groups()->count(), 1);
444
445
		$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
446
		$this->assertEquals($grouplessMember->Groups()->count(), 2);
447
448
		$group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
449
		$this->assertNotNull($group);
450
		$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
451
		$this->assertEquals($group->Title, 'New Group');
452
453
		$grouplessMember->removeFromGroupByCode('memberless');
454
		$this->assertEquals($memberlessGroup->Members()->count(), 0);
455
		$this->assertEquals($grouplessMember->Groups()->count(), 1);
456
457
		$grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
458
		$this->assertEquals($grouplessMember->Groups()->count(), 0);
459
	}
460
461
	public function testInGroup() {
462
		$staffmember = $this->objFromFixture(Member::class, 'staffmember');
463
		$managementmember = $this->objFromFixture(Member::class, 'managementmember');
464
		$accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
465
		$ceomember = $this->objFromFixture(Member::class, 'ceomember');
466
467
		$staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
468
		$managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
469
		$accountinggroup = $this->objFromFixture(Group::class, 'accountinggroup');
470
		$ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
471
472
		$this->assertTrue(
473
			$staffmember->inGroup($staffgroup),
474
			'Direct group membership is detected'
475
		);
476
		$this->assertTrue(
477
			$managementmember->inGroup($staffgroup),
478
			'Users of child group are members of a direct parent group (if not in strict mode)'
479
		);
480
		$this->assertTrue(
481
			$accountingmember->inGroup($staffgroup),
482
			'Users of child group are members of a direct parent group (if not in strict mode)'
483
		);
484
		$this->assertTrue(
485
			$ceomember->inGroup($staffgroup),
486
			'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
487
		);
488
		$this->assertTrue(
489
			$ceomember->inGroup($ceogroup, true),
490
			'Direct group membership is dected (if in strict mode)'
491
		);
492
		$this->assertFalse(
493
			$ceomember->inGroup($staffgroup, true),
494
			'Users of child group are not members of a direct parent group (if in strict mode)'
495
		);
496
		$this->assertFalse(
497
			$staffmember->inGroup($managementgroup),
498
			'Users of parent group are not members of a direct child group'
499
		);
500
		$this->assertFalse(
501
			$staffmember->inGroup($ceogroup),
502
			'Users of parent group are not members of an indirect grandchild group'
503
		);
504
		$this->assertFalse(
505
			$accountingmember->inGroup($managementgroup),
506
			'Users of group are not members of any siblings'
507
		);
508
		$this->assertFalse(
509
			$staffmember->inGroup('does-not-exist'),
510
			'Non-existant group returns false'
511
		);
512
	}
513
514
	/**
515
	 * Tests that the user is able to view their own record, and in turn, they can
516
	 * edit and delete their own record too.
517
	 */
518
	public function testCanManipulateOwnRecord() {
519
		$extensions = $this->removeExtensions(Object::get_extensions(Member::class));
520
		$member = $this->objFromFixture(Member::class, 'test');
521
		$member2 = $this->objFromFixture(Member::class, 'staffmember');
522
523
		$this->session()->inst_set('loggedInAs', null);
524
525
		/* Not logged in, you can't view, delete or edit the record */
526
		$this->assertFalse($member->canView());
527
		$this->assertFalse($member->canDelete());
528
		$this->assertFalse($member->canEdit());
529
530
		/* Logged in users can edit their own record */
531
		$this->session()->inst_set('loggedInAs', $member->ID);
532
		$this->assertTrue($member->canView());
533
		$this->assertFalse($member->canDelete());
534
		$this->assertTrue($member->canEdit());
535
536
		/* Other uses cannot view, delete or edit others records */
537
		$this->session()->inst_set('loggedInAs', $member2->ID);
538
		$this->assertFalse($member->canView());
539
		$this->assertFalse($member->canDelete());
540
		$this->assertFalse($member->canEdit());
541
542
		$this->addExtensions($extensions);
543
		$this->session()->inst_set('loggedInAs', null);
544
	}
545
546
	public function testAuthorisedMembersCanManipulateOthersRecords() {
547
		$extensions = $this->removeExtensions(Object::get_extensions(Member::class));
548
		$member = $this->objFromFixture(Member::class, 'test');
549
		$member2 = $this->objFromFixture(Member::class, 'staffmember');
550
551
		/* Group members with SecurityAdmin permissions can manipulate other records */
552
		$this->session()->inst_set('loggedInAs', $member->ID);
553
		$this->assertTrue($member2->canView());
554
		$this->assertTrue($member2->canDelete());
555
		$this->assertTrue($member2->canEdit());
556
557
		$this->addExtensions($extensions);
558
		$this->session()->inst_set('loggedInAs', null);
559
	}
560
561
	public function testExtendedCan() {
562
		$extensions = $this->removeExtensions(Object::get_extensions(Member::class));
563
		$member = $this->objFromFixture(Member::class, 'test');
564
565
		/* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
566
		$this->assertFalse($member->canView());
567
		$this->assertFalse($member->canDelete());
568
		$this->assertFalse($member->canEdit());
569
570
		/* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
571
		Member::add_extension(MemberTest\ViewingAllowedExtension::class);
572
		$member2 = $this->objFromFixture(Member::class, 'staffmember');
573
574
		$this->assertTrue($member2->canView());
575
		$this->assertFalse($member2->canDelete());
576
		$this->assertFalse($member2->canEdit());
577
578
		/* Apply a extension that denies viewing of the Member */
579
		Member::remove_extension(MemberTest\ViewingAllowedExtension::class);
580
		Member::add_extension(MemberTest\ViewingDeniedExtension::class);
581
		$member3 = $this->objFromFixture(Member::class, 'managementmember');
582
583
		$this->assertFalse($member3->canView());
584
		$this->assertFalse($member3->canDelete());
585
		$this->assertFalse($member3->canEdit());
586
587
		/* Apply a extension that allows viewing and editing but denies deletion */
588
		Member::remove_extension(MemberTest\ViewingDeniedExtension::class);
589
		Member::add_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
590
		$member4 = $this->objFromFixture(Member::class, 'accountingmember');
591
592
		$this->assertTrue($member4->canView());
593
		$this->assertFalse($member4->canDelete());
594
		$this->assertTrue($member4->canEdit());
595
596
		Member::remove_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
597
		$this->addExtensions($extensions);
598
	}
599
600
	/**
601
	 * Tests for {@link Member::getName()} and {@link Member::setName()}
602
	 */
603
	public function testName() {
604
		$member = $this->objFromFixture(Member::class, 'test');
605
		$member->setName('Test Some User');
606
		$this->assertEquals('Test Some User', $member->getName());
607
		$member->setName('Test');
608
		$this->assertEquals('Test', $member->getName());
609
		$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...
610
		$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...
611
		$this->assertEquals('Test', $member->getName());
612
	}
613
614
	public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves() {
615
		$adminMember = $this->objFromFixture(Member::class, 'admin');
616
		$otherAdminMember = $this->objFromFixture(Member::class, 'other-admin');
617
		$securityAdminMember = $this->objFromFixture(Member::class, 'test');
618
		$ceoMember = $this->objFromFixture(Member::class, 'ceomember');
619
620
		// Careful: Don't read as english language.
621
		// More precisely this should read canBeEditedBy()
622
623
		$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 615 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...
624
		$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 615 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...
625
		$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 615 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...
626
627
		$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 617 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...
628
		$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 617 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...
629
		$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 617 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...
630
	}
631
632
	public function testOnChangeGroups() {
633
		$staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
634
		$staffMember = $this->objFromFixture(Member::class, 'staffmember');
635
		$adminMember = $this->objFromFixture(Member::class, 'admin');
636
		$newAdminGroup = new Group(array('Title' => 'newadmin'));
637
		$newAdminGroup->write();
638
		Permission::grant($newAdminGroup->ID, 'ADMIN');
639
		$newOtherGroup = new Group(array('Title' => 'othergroup'));
640
		$newOtherGroup->write();
641
642
		$this->assertTrue(
643
			$staffMember->onChangeGroups(array($staffGroup->ID)),
644
			'Adding existing non-admin group relation is allowed for non-admin members'
645
		);
646
		$this->assertTrue(
647
			$staffMember->onChangeGroups(array($newOtherGroup->ID)),
648
			'Adding new non-admin group relation is allowed for non-admin members'
649
		);
650
		$this->assertFalse(
651
			$staffMember->onChangeGroups(array($newAdminGroup->ID)),
652
			'Adding new admin group relation is not allowed for non-admin members'
653
		);
654
655
		$this->session()->inst_set('loggedInAs', $adminMember->ID);
656
		$this->assertTrue(
657
			$staffMember->onChangeGroups(array($newAdminGroup->ID)),
658
			'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
659
		);
660
		$this->session()->inst_set('loggedInAs', null);
661
662
		$this->assertTrue(
663
			$adminMember->onChangeGroups(array($newAdminGroup->ID)),
664
			'Adding new admin group relation is allowed for admin members'
665
		);
666
	}
667
668
	/**
669
	 * Test Member_GroupSet::add
670
	 */
671
	public function testOnChangeGroupsByAdd() {
672
		$staffMember = $this->objFromFixture(Member::class, 'staffmember');
673
		$adminMember = $this->objFromFixture(Member::class, 'admin');
674
675
		// Setup new admin group
676
		$newAdminGroup = new Group(array('Title' => 'newadmin'));
677
		$newAdminGroup->write();
678
		Permission::grant($newAdminGroup->ID, 'ADMIN');
679
680
		// Setup non-admin group
681
		$newOtherGroup = new Group(array('Title' => 'othergroup'));
682
		$newOtherGroup->write();
683
684
		// Test staff can be added to other group
685
		$this->assertFalse($staffMember->inGroup($newOtherGroup));
686
		$staffMember->Groups()->add($newOtherGroup);
687
		$this->assertTrue(
688
			$staffMember->inGroup($newOtherGroup),
689
			'Adding new non-admin group relation is allowed for non-admin members'
690
		);
691
692
		// Test staff member can't be added to admin groups
693
		$this->assertFalse($staffMember->inGroup($newAdminGroup));
694
		$staffMember->Groups()->add($newAdminGroup);
695
		$this->assertFalse(
696
			$staffMember->inGroup($newAdminGroup),
697
			'Adding new admin group relation is not allowed for non-admin members'
698
		);
699
700
		// Test staff member can be added to admin group by admins
701
		$this->logInAs($adminMember);
702
		$staffMember->Groups()->add($newAdminGroup);
703
		$this->assertTrue(
704
			$staffMember->inGroup($newAdminGroup),
705
			'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
706
		);
707
708
		// Test staff member can be added if they are already admin
709
		$this->session()->inst_set('loggedInAs', null);
710
		$this->assertFalse($adminMember->inGroup($newAdminGroup));
711
		$adminMember->Groups()->add($newAdminGroup);
712
		$this->assertTrue(
713
			$adminMember->inGroup($newAdminGroup),
714
			'Adding new admin group relation is allowed for admin members'
715
		);
716
	}
717
718
	/**
719
	 * Test Member_GroupSet::add
720
	 */
721
	public function testOnChangeGroupsBySetIDList() {
722
		$staffMember = $this->objFromFixture(Member::class, 'staffmember');
723
724
		// Setup new admin group
725
		$newAdminGroup = new Group(array('Title' => 'newadmin'));
726
		$newAdminGroup->write();
727
		Permission::grant($newAdminGroup->ID, 'ADMIN');
728
729
		// Test staff member can't be added to admin groups
730
		$this->assertFalse($staffMember->inGroup($newAdminGroup));
731
		$staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
732
		$this->assertFalse(
733
			$staffMember->inGroup($newAdminGroup),
734
			'Adding new admin group relation is not allowed for non-admin members'
735
		);
736
	}
737
738
	/**
739
	 * Test that extensions using updateCMSFields() are applied correctly
740
	 */
741
	public function testUpdateCMSFields() {
742
		Member::add_extension(FieldsExtension::class);
743
744
		$member = Member::singleton();
745
		$fields = $member->getCMSFields();
746
747
		/** @skipUpgrade */
748
		$this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
749
		$this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
750
		$this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
751
752
		Member::remove_extension(FieldsExtension::class);
753
	}
754
755
	/**
756
	 * Test that all members are returned
757
	 */
758
	public function testMap_in_groupsReturnsAll() {
759
		$members = Member::map_in_groups();
760
		$this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
761
	}
762
763
	/**
764
	 * Test that only admin members are returned
765
	 */
766
	public function testMap_in_groupsReturnsAdmins() {
767
		$adminID = $this->objFromFixture(Group::class, 'admingroup')->ID;
768
		$members = Member::map_in_groups($adminID)->toArray();
769
770
		$admin = $this->objFromFixture(Member::class, 'admin');
771
		$otherAdmin = $this->objFromFixture(Member::class, 'other-admin');
772
773
		$this->assertTrue(in_array($admin->getTitle(), $members),
774
			$admin->getTitle().' should be in the returned list.');
775
		$this->assertTrue(in_array($otherAdmin->getTitle(), $members),
776
			$otherAdmin->getTitle().' should be in the returned list.');
777
		$this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
778
	}
779
780
	/**
781
	 * Add the given array of member extensions as class names.
782
	 * This is useful for re-adding extensions after being removed
783
	 * in a test case to produce an unbiased test.
784
	 *
785
	 * @param array $extensions
786
	 * @return array The added extensions
787
	 */
788
	protected function addExtensions($extensions) {
789
		if($extensions) foreach($extensions as $extension) {
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...
790
			Member::add_extension($extension);
791
		}
792
		return $extensions;
793
	}
794
795
	/**
796
	 * Remove given extensions from Member. This is useful for
797
	 * removing extensions that could produce a biased
798
	 * test result, as some extensions applied by project
799
	 * code or modules can do this.
800
	 *
801
	 * @param array $extensions
802
	 * @return array The removed extensions
803
	 */
804
	protected function removeExtensions($extensions) {
805
		if($extensions) foreach($extensions as $extension) {
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...
806
			Member::remove_extension($extension);
807
		}
808
		return $extensions;
809
	}
810
811
	public function testGenerateAutologinTokenAndStoreHash() {
812
		$enc = new PasswordEncryptor_Blowfish();
813
814
		$m = new Member();
815
		$m->PasswordEncryption = 'blowfish';
816
		$m->Salt = $enc->salt('123');
817
818
		$token = $m->generateAutologinTokenAndStoreHash();
819
820
		$this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as ahash.');
821
	}
822
823
	public function testValidateAutoLoginToken() {
824
		$enc = new PasswordEncryptor_Blowfish();
825
826
		$m1 = new Member();
827
		$m1->PasswordEncryption = 'blowfish';
828
		$m1->Salt = $enc->salt('123');
829
		$m1Token = $m1->generateAutologinTokenAndStoreHash();
830
831
		$m2 = new Member();
832
		$m2->PasswordEncryption = 'blowfish';
833
		$m2->Salt = $enc->salt('456');
834
		$m2Token = $m2->generateAutologinTokenAndStoreHash();
835
836
		$this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
837
		$this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
838
	}
839
840
	public function testRememberMeHashGeneration() {
841
		$m1 = $this->objFromFixture(Member::class, 'grouplessmember');
842
843
		$m1->login(true);
844
		$hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
845
		$this->assertEquals($hashes->count(), 1);
846
		$firstHash = $hashes->first();
847
		$this->assertNotNull($firstHash->DeviceID);
848
		$this->assertNotNull($firstHash->Hash);
849
	}
850
851
	public function testRememberMeHashAutologin() {
852
		/** @var Member $m1 */
853
		$m1 = $this->objFromFixture(Member::class, 'noexpiry');
854
855
		$m1->logIn(true);
856
		$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
857
		$this->assertNotNull($firstHash);
858
859
		// re-generates the hash so we can get the token
860
		$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...
861
		$token = $firstHash->getToken();
862
		$firstHash->write();
863
864
		$response = $this->get(
865
			'Security/login',
866
			$this->session(),
867
			null,
868
			array(
869
				'alc_enc' => $m1->ID.':'.$token,
870
				'alc_device' => $firstHash->DeviceID
871
			)
872
		);
873
		$message = _t(
874
			'Member.LOGGEDINAS',
875
			"You're logged in as {name}.",
876
			array('name' => $m1->FirstName)
877
		);
878
		$this->assertContains($message, $response->getBody());
879
880
		$this->session()->inst_set('loggedInAs', null);
881
882
		// A wrong token or a wrong device ID should not let us autologin
883
		$response = $this->get(
884
			'Security/login',
885
			$this->session(),
886
			null,
887
			array(
888
				'alc_enc' => $m1->ID.':'.str_rot13($token),
889
				'alc_device' => $firstHash->DeviceID
890
			)
891
		);
892
		$this->assertNotContains($message, $response->getBody());
893
894
		$response = $this->get(
895
			'Security/login',
896
			$this->session(),
897
			null,
898
			array(
899
				'alc_enc' => $m1->ID.':'.$token,
900
				'alc_device' => str_rot13($firstHash->DeviceID)
901
			)
902
		);
903
		$this->assertNotContains($message, $response->getBody());
904
905
		// Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
906
		// should remove all previous hashes for this device
907
		$response = $this->post(
908
			'Security/LoginForm',
909
			array(
910
				'Email' => $m1->Email,
911
				'Password' => '1nitialPassword',
912
				'AuthenticationMethod' => MemberAuthenticator::class,
913
				'action_dologin' => 'action_dologin'
914
			),
915
			null,
916
			$this->session(),
917
			null,
918
			array(
919
				'alc_device' => $firstHash->DeviceID
920
			)
921
		);
922
		$this->assertContains($message, $response->getBody());
923
		$this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), 0);
924
	}
925
926
	public function testExpiredRememberMeHashAutologin() {
927
		$m1 = $this->objFromFixture(Member::class, 'noexpiry');
928
929
		$m1->login(true);
930
		$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
931
		$this->assertNotNull($firstHash);
932
933
		// re-generates the hash so we can get the token
934
		$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...
935
		$token = $firstHash->getToken();
936
		$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...
937
		$firstHash->write();
938
939
		DBDateTime::set_mock_now('1999-12-31 23:59:59');
940
941
		$response = $this->get(
942
			'Security/login',
943
			$this->session(),
944
			null,
945
			array(
946
				'alc_enc' => $m1->ID.':'.$token,
947
				'alc_device' => $firstHash->DeviceID
948
			)
949
		);
950
		$message = _t(
951
			'Member.LOGGEDINAS',
952
			"You're logged in as {name}.",
953
			array('name' => $m1->FirstName)
954
		);
955
		$this->assertContains($message, $response->getBody());
956
957
		$this->session()->inst_set('loggedInAs', null);
958
959
		// re-generates the hash so we can get the token
960
		$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...
961
		$token = $firstHash->getToken();
962
		$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...
963
		$firstHash->write();
964
965
		DBDatetime::set_mock_now('2000-01-01 00:00:01');
966
967
		$response = $this->get(
968
			'Security/login',
969
			$this->session(),
970
			null,
971
			array(
972
				'alc_enc' => $m1->ID.':'.$token,
973
				'alc_device' => $firstHash->DeviceID
974
			)
975
		);
976
		$this->assertNotContains($message, $response->getBody());
977
		$this->session()->inst_set('loggedInAs', null);
978
		DBDatetime::clear_mock_now();
979
	}
980
981
	public function testRememberMeMultipleDevices() {
982
		$m1 = $this->objFromFixture(Member::class, 'noexpiry');
983
984
		// First device
985
		$m1->login(true);
986
		Cookie::set('alc_device', null);
987
		// Second device
988
		$m1->login(true);
989
990
		// Hash of first device
991
		$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
992
		$this->assertNotNull($firstHash);
993
994
		// Hash of second device
995
		$secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last();
996
		$this->assertNotNull($secondHash);
997
998
		// DeviceIDs are different
999
		$this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
1000
1001
		// re-generates the hashes so we can get the tokens
1002
		$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...
1003
		$firstToken = $firstHash->getToken();
1004
		$firstHash->write();
1005
1006
		$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...
1007
		$secondToken = $secondHash->getToken();
1008
		$secondHash->write();
1009
1010
		// Accessing the login page should show the user's name straight away
1011
		$response = $this->get(
1012
			'Security/login',
1013
			$this->session(),
1014
			null,
1015
			array(
1016
				'alc_enc' => $m1->ID.':'.$firstToken,
1017
				'alc_device' => $firstHash->DeviceID
1018
			)
1019
		);
1020
		$message = _t(
1021
			'Member.LOGGEDINAS',
1022
			"You're logged in as {name}.",
1023
			array('name' => $m1->FirstName)
1024
		);
1025
		$this->assertContains($message, $response->getBody());
1026
1027
		$this->session()->inst_set('loggedInAs', null);
1028
1029
		// Accessing the login page from the second device
1030
		$response = $this->get(
1031
			'Security/login',
1032
			$this->session(),
1033
			null,
1034
			array(
1035
				'alc_enc' => $m1->ID.':'.$secondToken,
1036
				'alc_device' => $secondHash->DeviceID
1037
			)
1038
		);
1039
		$this->assertContains($message, $response->getBody());
1040
1041
		// Logging out from the second device - only one device being logged out
1042
		RememberLoginHash::config()->update('logout_across_devices', false);
1043
		$response = $this->get(
1044
			'Security/logout',
1045
			$this->session(),
1046
			null,
1047
			array(
1048
				'alc_enc' => $m1->ID.':'.$secondToken,
1049
				'alc_device' => $secondHash->DeviceID
1050
			)
1051
		);
1052
		$this->assertEquals(
1053
			RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->count(),
1054
			1
1055
		);
1056
1057
		// Logging out from any device when all login hashes should be removed
1058
		RememberLoginHash::config()->update('logout_across_devices', true);
1059
		$m1->login(true);
1060
		$response = $this->get('Security/logout', $this->session());
1061
		$this->assertEquals(
1062
			RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
1063
			0
1064
		);
1065
	}
1066
1067
	public function testCanDelete() {
1068
		$admin1 = $this->objFromFixture(Member::class, 'admin');
1069
		$admin2 = $this->objFromFixture(Member::class, 'other-admin');
1070
		$member1 = $this->objFromFixture(Member::class, 'grouplessmember');
1071
		$member2 = $this->objFromFixture(Member::class, 'noformatmember');
1072
1073
		$this->assertTrue(
1074
			$admin1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture(\S...::class, 'other-admin') on line 1069 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...
1075
			'Admins can delete other admins'
1076
		);
1077
		$this->assertTrue(
1078
			$member1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture(\S...::class, 'other-admin') on line 1069 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...
1079
			'Admins can delete non-admins'
1080
		);
1081
		$this->assertFalse(
1082
			$admin1->canDelete($admin1),
0 ignored issues
show
Bug introduced by
It seems like $admin1 defined by $this->objFromFixture(\S...Member::class, 'admin') on line 1068 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...
1083
			'Admins can not delete themselves'
1084
		);
1085
		$this->assertFalse(
1086
			$member1->canDelete($member2),
0 ignored issues
show
Bug introduced by
It seems like $member2 defined by $this->objFromFixture(\S...lass, 'noformatmember') on line 1071 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...
1087
			'Non-admins can not delete other non-admins'
1088
		);
1089
		$this->assertFalse(
1090
			$member1->canDelete($member1),
0 ignored issues
show
Bug introduced by
It seems like $member1 defined by $this->objFromFixture(\S...ass, 'grouplessmember') on line 1070 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...
1091
			'Non-admins can not delete themselves'
1092
		);
1093
	}
1094
1095
	public function testFailedLoginCount() {
1096
		$maxFailedLoginsAllowed = 3;
1097
		//set up the config variables to enable login lockouts
1098
		Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
1099
1100
		$member = $this->objFromFixture(Member::class, 'test');
1101
		$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...
1102
1103
		for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
1104
			$member->registerFailedLogin();
1105
1106
			$this->assertEquals(
1107
				++$failedLoginCount,
1108
				$member->FailedLoginCount,
1109
				'Failed to increment $member->FailedLoginCount'
1110
			);
1111
1112
			$this->assertFalse(
1113
				$member->isLockedOut(),
1114
				"Member has been locked out too early"
1115
			);
1116
		}
1117
	}
1118
1119
	public function testMemberValidator()
1120
	{
1121
		// clear custom requirements for this test
1122
		Member_Validator::config()->update('customRequired', null);
1123
		$memberA = $this->objFromFixture(Member::class, 'admin');
1124
		$memberB = $this->objFromFixture(Member::class, 'test');
1125
1126
		// create a blank form
1127
		$form = new MemberTest\ValidatorForm();
1128
1129
		$validator = new Member_Validator();
1130
		$validator->setForm($form);
1131
1132
		// Simulate creation of a new member via form, but use an existing member identifier
1133
		$fail = $validator->php(array(
1134
			'FirstName' => 'Test',
1135
			'Email' => $memberA->Email
1136
		));
1137
1138
		$this->assertFalse(
1139
			$fail,
1140
			'Member_Validator must fail when trying to create new Member with existing Email.'
1141
		);
1142
1143
		// populate the form with values from another member
1144
		$form->loadDataFrom($memberB);
0 ignored issues
show
Bug introduced by
It seems like $memberB defined by $this->objFromFixture(\S...\Member::class, 'test') on line 1124 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...
1145
1146
		// Assign the validator to an existing member
1147
		// (this is basically the same as passing the member ID with the form data)
1148
		$validator->setForMember($memberB);
1149
1150
		// Simulate update of a member via form and use an existing member Email
1151
		$fail = $validator->php(array(
1152
			'FirstName' => 'Test',
1153
			'Email' => $memberA->Email
1154
		));
1155
1156
		// Simulate update to a new Email address
1157
		$pass1 = $validator->php(array(
1158
			'FirstName' => 'Test',
1159
			'Email' => '[email protected]'
1160
		));
1161
1162
		// Pass in the same Email address that the member already has. Ensure that case is valid
1163
		$pass2 = $validator->php(array(
1164
			'FirstName' => 'Test',
1165
			'Surname' => 'User',
1166
			'Email' => $memberB->Email
1167
		));
1168
1169
		$this->assertFalse(
1170
			$fail,
1171
			'Member_Validator must fail when trying to update existing member with existing Email.'
1172
		);
1173
1174
		$this->assertTrue(
1175
			$pass1,
1176
			'Member_Validator must pass when Email is updated to a value that\'s not in use.'
1177
		);
1178
1179
		$this->assertTrue(
1180
			$pass2,
1181
			'Member_Validator must pass when Member updates his own Email to the already existing value.'
1182
		);
1183
	}
1184
1185
	public function testMemberValidatorWithExtensions()
1186
	{
1187
		// clear custom requirements for this test
1188
		Member_Validator::config()->update('customRequired', null);
1189
1190
		// create a blank form
1191
		$form = new MemberTest\ValidatorForm();
1192
1193
		// Test extensions
1194
		Member_Validator::add_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1195
		$validator = new Member_Validator();
1196
		$validator->setForm($form);
1197
1198
		// This test should fail, since the extension enforces FirstName == Surname
1199
		$fail = $validator->php(array(
1200
			'FirstName' => 'Test',
1201
			'Surname' => 'User',
1202
			'Email' => '[email protected]'
1203
		));
1204
1205
		$pass = $validator->php(array(
1206
			'FirstName' => 'Test',
1207
			'Surname' => 'Test',
1208
			'Email' => '[email protected]'
1209
		));
1210
1211
		$this->assertFalse(
1212
			$fail,
1213
			'Member_Validator must fail because of added extension.'
1214
		);
1215
1216
		$this->assertTrue(
1217
			$pass,
1218
			'Member_Validator must succeed, since it meets all requirements.'
1219
		);
1220
1221
		// Add another extension that always fails. This ensures that all extensions are considered in the validation
1222
		Member_Validator::add_extension(MemberTest\AlwaysFailExtension::class);
1223
		$validator = new Member_Validator();
1224
		$validator->setForm($form);
1225
1226
		// Even though the data is valid, This test should still fail, since one extension always returns false
1227
		$fail = $validator->php(array(
1228
			'FirstName' => 'Test',
1229
			'Surname' => 'Test',
1230
			'Email' => '[email protected]'
1231
		));
1232
1233
		$this->assertFalse(
1234
			$fail,
1235
			'Member_Validator must fail because of added extensions.'
1236
		);
1237
1238
		// Remove added extensions
1239
		Member_Validator::remove_extension(MemberTest\AlwaysFailExtension::class);
1240
		Member_Validator::remove_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1241
	}
1242
1243
	public function testCustomMemberValidator()
1244
	{
1245
		// clear custom requirements for this test
1246
		Member_Validator::config()->update('customRequired', null);
1247
1248
		$member = $this->objFromFixture(Member::class, 'admin');
1249
1250
		$form = new MemberTest\ValidatorForm();
1251
		$form->loadDataFrom($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture(\S...Member::class, 'admin') on line 1248 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...
1252
1253
		$validator = new Member_Validator();
1254
		$validator->setForm($form);
1255
1256
		$pass = $validator->php(array(
1257
			'FirstName' => 'Borris',
1258
			'Email' => '[email protected]'
1259
		));
1260
1261
		$fail = $validator->php(array(
1262
			'Email' => '[email protected]',
1263
			'Surname' => ''
1264
		));
1265
1266
		$this->assertTrue($pass, 'Validator requires a FirstName and Email');
1267
		$this->assertFalse($fail, 'Missing FirstName');
1268
1269
		$ext = new MemberTest\ValidatorExtension();
1270
		$ext->updateValidator($validator);
1271
1272
		$pass = $validator->php(array(
1273
			'FirstName' => 'Borris',
1274
			'Email' => '[email protected]'
1275
		));
1276
1277
		$fail = $validator->php(array(
1278
			'Email' => '[email protected]'
1279
		));
1280
1281
		$this->assertFalse($pass, 'Missing surname');
1282
		$this->assertFalse($fail, 'Missing surname value');
1283
1284
		$fail = $validator->php(array(
1285
			'Email' => '[email protected]',
1286
			'Surname' => 'Silverman'
1287
		));
1288
1289
		$this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1290
	}
1291
1292
	public function testCurrentUser() {
1293
		$this->assertNull(Member::currentUser());
1294
1295
		$adminMember = $this->objFromFixture(Member::class, 'admin');
1296
		$this->logInAs($adminMember);
1297
1298
		$userFromSession = Member::currentUser();
1299
		$this->assertEquals($adminMember->ID, $userFromSession->ID);
1300
	}
1301
1302
}
1303