Completed
Push — namespace-model ( 9b3f38...c67c40 )
by Sam
16:21 queued 05:15
created

MemberTest::testPasswordExpirySetting()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 17
rs 9.4285
cc 1
eloc 12
nc 1
nop 0
1
<?php
2
/**
3
 * @package framework
4
 * @subpackage tests
5
 */
6
7
use SilverStripe\Model\DataObject;
8
use SilverStripe\Model\DB;
9
use SilverStripe\Model\DataExtension;
10
class MemberTest extends FunctionalTest {
11
	protected static $fixture_file = 'MemberTest.yml';
12
13
	protected $orig = array();
14
	protected $local = null;
15
16
	protected $illegalExtensions = array(
17
		'Member' => array(
18
			// TODO Coupling with modules, this should be resolved by automatically
19
			// removing all applied extensions before a unit test
20
			'ForumRole',
21
			'OpenIDAuthenticatedRole'
22
		)
23
	);
24
25
	public function __construct() {
26
		parent::__construct();
27
28
		//Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
29
		//This is because the test relies on the yaml file being interpreted according to a particular date format
30
		//and this setup occurs before the setUp method is run
31
		$this->local = i18n::default_locale();
0 ignored issues
show
Deprecated Code introduced by
The method i18n::default_locale() has been deprecated with message: since version 4.0; Use the "i18n.default_locale" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
32
		i18n::set_default_locale('en_US');
0 ignored issues
show
Deprecated Code introduced by
The method i18n::set_default_locale() has been deprecated with message: since version 4.0; Use the "i18n.default_locale" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
33
	}
34
35
	public function __destruct() {
36
		i18n::set_default_locale($this->local);
0 ignored issues
show
Deprecated Code introduced by
The method i18n::set_default_locale() has been deprecated with message: since version 4.0; Use the "i18n.default_locale" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
37
	}
38
39
	public function setUp() {
40
		parent::setUp();
41
42
		$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<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...
43
		Member::config()->unique_identifier_field = 'Email';
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<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...
44
		Member::set_password_validator(null);
45
	}
46
47
	public function tearDown() {
48
		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<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...
49
		parent::tearDown();
50
	}
51
52
53
54
	/**
55
	 * @expectedException ValidationException
56
	 */
57 View Code Duplication
	public function testWriteDoesntMergeNewRecordWithExistingMember() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
58
		$m1 = new Member();
59
		$m1->Email = '[email protected]';
60
		$m1->write();
61
62
		$m2 = new Member();
63
		$m2->Email = '[email protected]';
64
		$m2->write();
65
	}
66
67
	/**
68
	 * @expectedException ValidationException
69
	 */
70 View Code Duplication
	public function testWriteDoesntMergeExistingMemberOnIdentifierChange() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
71
		$m1 = new Member();
72
		$m1->Email = '[email protected]';
73
		$m1->write();
74
75
		$m2 = new Member();
76
		$m2->Email = '[email protected]';
77
		$m2->write();
78
79
		$m2->Email = '[email protected]';
80
		$m2->write();
81
	}
82
83
	public function testDefaultPasswordEncryptionOnMember() {
84
		$memberWithPassword = new Member();
85
		$memberWithPassword->Password = 'mypassword';
86
		$memberWithPassword->write();
87
		$this->assertEquals(
88
			$memberWithPassword->PasswordEncryption,
89
			Security::config()->password_encryption_algorithm,
90
			'Password encryption is set for new member records on first write (with setting "Password")'
91
		);
92
93
		$memberNoPassword = new Member();
94
		$memberNoPassword->write();
95
		$this->assertNull(
96
			$memberNoPassword->PasswordEncryption,
97
			'Password encryption is not set for new member records on first write, when not setting a "Password")'
98
		);
99
	}
100
101
	public function testDefaultPasswordEncryptionDoesntChangeExistingMembers() {
102
		$member = new Member();
103
		$member->Password = 'mypassword';
104
		$member->PasswordEncryption = 'sha1_v2.4';
105
		$member->write();
106
107
		$origAlgo = Security::config()->password_encryption_algorithm;
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm does not exist on object<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...
108
		Security::config()->password_encryption_algorithm = 'none';
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm does not exist on object<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...
109
110
		$member->Password = 'mynewpassword';
111
		$member->write();
112
113
		$this->assertEquals(
114
			$member->PasswordEncryption,
115
			'sha1_v2.4'
116
		);
117
		$result = $member->checkPassword('mynewpassword');
118
		$this->assertTrue($result->valid());
119
120
		Security::config()->password_encryption_algorithm = $origAlgo;
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm does not exist on object<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...
121
	}
122
123
	public function testKeepsEncryptionOnEmptyPasswords() {
124
		$member = new Member();
125
		$member->Password = 'mypassword';
126
		$member->PasswordEncryption = 'sha1_v2.4';
127
		$member->write();
128
129
		$member->Password = '';
130
		$member->write();
131
132
		$this->assertEquals(
133
			$member->PasswordEncryption,
134
			'sha1_v2.4'
135
		);
136
		$result = $member->checkPassword('');
137
		$this->assertTrue($result->valid());
138
	}
139
140
	public function testSetPassword() {
141
		$member = $this->objFromFixture('Member', 'test');
142
		$member->Password = "test1";
0 ignored issues
show
Documentation introduced by
The property Password does not exist on object<SilverStripe\Model\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...
143
		$member->write();
144
		$result = $member->checkPassword('test1');
145
		$this->assertTrue($result->valid());
146
	}
147
148
	/**
149
	 * Test that password changes are logged properly
150
	 */
151
	public function testPasswordChangeLogging() {
152
		$member = $this->objFromFixture('Member', 'test');
153
		$this->assertNotNull($member);
154
		$member->Password = "test1";
0 ignored issues
show
Documentation introduced by
The property Password does not exist on object<SilverStripe\Model\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...
155
		$member->write();
156
157
		$member->Password = "test2";
0 ignored issues
show
Documentation introduced by
The property Password does not exist on object<SilverStripe\Model\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...
158
		$member->write();
159
160
		$member->Password = "test3";
0 ignored issues
show
Documentation introduced by
The property Password does not exist on object<SilverStripe\Model\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...
161
		$member->write();
162
163
		$passwords = DataObject::get("MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
164
			->getIterator();
165
		$this->assertNotNull($passwords);
166
		$passwords->rewind();
167
		$this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
168
169
		$passwords->next();
170
		$this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
171
172
		$passwords->next();
173
		$this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
174
175
		$passwords->next();
176
		$this->assertInstanceOf('SilverStripe\Model\DataObject', $passwords->current());
177
		$this->assertTrue($passwords->current()->checkPassword('1nitialPassword'),
178
			"Password 1nitialPassword not found in MemberRecord");
179
180
		//check we don't retain orphaned records when a member is deleted
181
		$member->delete();
182
183
		$passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
184
185
		$this->assertCount(0, $passwords);
186
	}
187
188
	/**
189
	 * Test that changed passwords will send an email
190
	 */
191
	public function testChangedPasswordEmaling() {
192
		$oldSetting = Config::inst()->get('Member', 'notify_password_change');
193
		Config::inst()->update('Member', 'notify_password_change', true);
194
195
		$this->clearEmails();
196
197
		$member = $this->objFromFixture('Member', 'test');
198
		$this->assertNotNull($member);
199
		$valid = $member->changePassword('32asDF##$$%%');
200
		$this->assertTrue($valid->valid());
201
202
		$this->assertEmailSent("[email protected]", null, "Your password has been changed",
203
			'/dummy@silverstripe\.tld/');
204
205
		Config::inst()->update('Member', 'notify_password_change', $oldSetting);
206
	}
207
208
	/**
209
	 * Test that triggering "forgotPassword" sends an Email with a reset link
210
	 */
211
	public function testForgotPasswordEmaling() {
212
		$this->clearEmails();
213
		$this->autoFollowRedirection = false;
214
215
		$member = $this->objFromFixture('Member', 'test');
216
		$this->assertNotNull($member);
217
218
		// Initiate a password-reset
219
		$response = $this->post('Security/LostPasswordForm', array('Email' => $member->Email));
220
221
		$this->assertEquals($response->getStatusCode(), 302);
222
223
		// We should get redirected to Security/passwordsent
224
		$this->assertContains('Security/passwordsent/[email protected]',
225
			urldecode($response->getHeader('Location')));
226
227
		// Check existance of reset link
228
		$this->assertEmailSent("[email protected]", null, 'Your password reset link',
229
			'/Security\/changepassword\?m='.$member->ID.'&t=[^"]+/');
230
	}
231
232
	/**
233
	 * Test that passwords validate against NZ e-government guidelines
234
	 *  - don't allow the use of the last 6 passwords
235
	 *  - require at least 3 of lowercase, uppercase, digits and punctuation
236
	 *  - at least 7 characters long
237
	 */
238
	public function testValidatePassword() {
239
		$member = $this->objFromFixture('Member', 'test');
240
		$this->assertNotNull($member);
241
242
		Member::set_password_validator(new MemberTest_PasswordValidator());
243
244
		// BAD PASSWORDS
245
246
		$valid = $member->changePassword('shorty');
247
		$this->assertFalse($valid->valid());
248
		$this->assertContains("TOO_SHORT", $valid->codeList());
249
250
		$valid = $member->changePassword('longone');
251
		$this->assertNotContains("TOO_SHORT", $valid->codeList());
252
		$this->assertContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
253
		$this->assertFalse($valid->valid());
254
255
		$valid = $member->changePassword('w1thNumb3rs');
256
		$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
257
		$this->assertTrue($valid->valid());
258
259
		// Clear out the MemberPassword table to ensure that the system functions properly in that situation
260
		DB::query("DELETE FROM \"MemberPassword\"");
261
262
		// GOOD PASSWORDS
263
264
		$valid = $member->changePassword('withSym###Ls');
265
		$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
266
		$this->assertTrue($valid->valid());
267
268
		$valid = $member->changePassword('withSym###Ls2');
269
		$this->assertTrue($valid->valid());
270
271
		$valid = $member->changePassword('withSym###Ls3');
272
		$this->assertTrue($valid->valid());
273
274
		$valid = $member->changePassword('withSym###Ls4');
275
		$this->assertTrue($valid->valid());
276
277
		$valid = $member->changePassword('withSym###Ls5');
278
		$this->assertTrue($valid->valid());
279
280
		$valid = $member->changePassword('withSym###Ls6');
281
		$this->assertTrue($valid->valid());
282
283
		$valid = $member->changePassword('withSym###Ls7');
284
		$this->assertTrue($valid->valid());
285
286
		// CAN'T USE PASSWORDS 2-7, but I can use pasword 1
287
288
		$valid = $member->changePassword('withSym###Ls2');
289
		$this->assertFalse($valid->valid());
290
		$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
291
292
		$valid = $member->changePassword('withSym###Ls5');
293
		$this->assertFalse($valid->valid());
294
		$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
295
296
		$valid = $member->changePassword('withSym###Ls7');
297
		$this->assertFalse($valid->valid());
298
		$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
299
300
		$valid = $member->changePassword('withSym###Ls');
301
		$this->assertTrue($valid->valid());
302
303
		// HAVING DONE THAT, PASSWORD 2 is now available from the list
304
305
		$valid = $member->changePassword('withSym###Ls2');
306
		$this->assertTrue($valid->valid());
307
308
		$valid = $member->changePassword('withSym###Ls3');
309
		$this->assertTrue($valid->valid());
310
311
		$valid = $member->changePassword('withSym###Ls4');
312
		$this->assertTrue($valid->valid());
313
314
		Member::set_password_validator(null);
315
	}
316
317
	/**
318
	 * Test that the PasswordExpiry date is set when passwords are changed
319
	 */
320
	public function testPasswordExpirySetting() {
321
		Member::config()->password_expiry_days = 90;
0 ignored issues
show
Documentation introduced by
The property password_expiry_days does not exist on object<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...
322
323
		$member = $this->objFromFixture('Member', 'test');
324
		$this->assertNotNull($member);
325
		$valid = $member->changePassword("Xx?1234234");
326
		$this->assertTrue($valid->valid());
327
328
		$expiryDate = date('Y-m-d', time() + 90*86400);
329
		$this->assertEquals($expiryDate, $member->PasswordExpiry);
330
331
		Member::config()->password_expiry_days = null;
0 ignored issues
show
Documentation introduced by
The property password_expiry_days does not exist on object<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...
332
		$valid = $member->changePassword("Xx?1234235");
333
		$this->assertTrue($valid->valid());
334
335
		$this->assertNull($member->PasswordExpiry);
336
	}
337
338
	public function testIsPasswordExpired() {
339
		$member = $this->objFromFixture('Member', 'test');
340
		$this->assertNotNull($member);
341
		$this->assertFalse($member->isPasswordExpired());
342
343
		$member = $this->objFromFixture('Member', 'noexpiry');
344
		$member->PasswordExpiry = null;
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry does not exist on object<SilverStripe\Model\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...
345
		$this->assertFalse($member->isPasswordExpired());
346
347
		$member = $this->objFromFixture('Member', 'expiredpassword');
348
		$this->assertTrue($member->isPasswordExpired());
349
350
		// Check the boundary conditions
351
		// If PasswordExpiry == today, then it's expired
352
		$member->PasswordExpiry = date('Y-m-d');
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry does not exist on object<SilverStripe\Model\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...
353
		$this->assertTrue($member->isPasswordExpired());
354
355
		// If PasswordExpiry == tomorrow, then it's not
356
		$member->PasswordExpiry = date('Y-m-d', time() + 86400);
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry does not exist on object<SilverStripe\Model\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...
357
		$this->assertFalse($member->isPasswordExpired());
358
359
	}
360
361
	public function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() {
362
		Config::inst()->update('i18n', 'date_format', 'yyyy-MM-dd');
363
		Config::inst()->update('i18n', 'time_format', 'H:mm');
364
		$member = $this->objFromFixture('Member', 'noformatmember');
365
		$this->assertEquals('yyyy-MM-dd', $member->DateFormat);
366
		$this->assertEquals('H:mm', $member->TimeFormat);
367
	}
368
369
	public function testInGroups() {
370
		$staffmember = $this->objFromFixture('Member', 'staffmember');
371
		$managementmember = $this->objFromFixture('Member', 'managementmember');
0 ignored issues
show
Unused Code introduced by
$managementmember is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
372
		$accountingmember = $this->objFromFixture('Member', 'accountingmember');
0 ignored issues
show
Unused Code introduced by
$accountingmember is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
373
		$ceomember = $this->objFromFixture('Member', 'ceomember');
374
375
		$staffgroup = $this->objFromFixture('Group', 'staffgroup');
376
		$managementgroup = $this->objFromFixture('Group', 'managementgroup');
377
		$accountinggroup = $this->objFromFixture('Group', 'accountinggroup');
0 ignored issues
show
Unused Code introduced by
$accountinggroup is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
378
		$ceogroup = $this->objFromFixture('Group', 'ceogroup');
379
380
		$this->assertTrue(
381
			$staffmember->inGroups(array($staffgroup, $managementgroup)),
382
			'inGroups() succeeds if a membership is detected on one of many passed groups'
383
		);
384
		$this->assertFalse(
385
			$staffmember->inGroups(array($ceogroup, $managementgroup)),
386
			'inGroups() fails if a membership is detected on none of the passed groups'
387
		);
388
		$this->assertFalse(
389
			$ceomember->inGroups(array($staffgroup, $managementgroup), true),
390
			'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
391
		);
392
	}
393
394
	public function testAddToGroupByCode() {
395
		$grouplessMember = $this->objFromFixture('Member', 'grouplessmember');
396
		$memberlessGroup = $this->objFromFixture('Group','memberlessgroup');
397
398
		$this->assertFalse($grouplessMember->Groups()->exists());
399
		$this->assertFalse($memberlessGroup->Members()->exists());
400
401
		$grouplessMember->addToGroupByCode('memberless');
402
403
		$this->assertEquals($memberlessGroup->Members()->Count(), 1);
404
		$this->assertEquals($grouplessMember->Groups()->Count(), 1);
405
406
		$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
407
		$this->assertEquals($grouplessMember->Groups()->Count(), 2);
408
409
		$group = DataObject::get_one('Group', array(
410
			'"Group"."Code"' => 'somegroupthatwouldneverexist'
411
		));
412
		$this->assertNotNull($group);
413
		$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
414
		$this->assertEquals($group->Title, 'New Group');
415
416
	}
417
418
	public function testRemoveFromGroupByCode() {
419
		$grouplessMember = $this->objFromFixture('Member', 'grouplessmember');
420
		$memberlessGroup = $this->objFromFixture('Group','memberlessgroup');
421
422
		$this->assertFalse($grouplessMember->Groups()->exists());
423
		$this->assertFalse($memberlessGroup->Members()->exists());
424
425
		$grouplessMember->addToGroupByCode('memberless');
426
427
		$this->assertEquals($memberlessGroup->Members()->Count(), 1);
428
		$this->assertEquals($grouplessMember->Groups()->Count(), 1);
429
430
		$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
431
		$this->assertEquals($grouplessMember->Groups()->Count(), 2);
432
433
		$group = DataObject::get_one('Group', "\"Code\" = 'somegroupthatwouldneverexist'");
434
		$this->assertNotNull($group);
435
		$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
436
		$this->assertEquals($group->Title, 'New Group');
437
438
		$grouplessMember->removeFromGroupByCode('memberless');
439
		$this->assertEquals($memberlessGroup->Members()->Count(), 0);
440
		$this->assertEquals($grouplessMember->Groups()->Count(), 1);
441
442
		$grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
443
		$this->assertEquals($grouplessMember->Groups()->Count(), 0);
444
	}
445
446
	public function testInGroup() {
447
		$staffmember = $this->objFromFixture('Member', 'staffmember');
448
		$managementmember = $this->objFromFixture('Member', 'managementmember');
449
		$accountingmember = $this->objFromFixture('Member', 'accountingmember');
450
		$ceomember = $this->objFromFixture('Member', 'ceomember');
451
452
		$staffgroup = $this->objFromFixture('Group', 'staffgroup');
453
		$managementgroup = $this->objFromFixture('Group', 'managementgroup');
454
		$accountinggroup = $this->objFromFixture('Group', 'accountinggroup');
0 ignored issues
show
Unused Code introduced by
$accountinggroup is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
455
		$ceogroup = $this->objFromFixture('Group', 'ceogroup');
456
457
		$this->assertTrue(
458
			$staffmember->inGroup($staffgroup),
459
			'Direct group membership is detected'
460
		);
461
		$this->assertTrue(
462
			$managementmember->inGroup($staffgroup),
463
			'Users of child group are members of a direct parent group (if not in strict mode)'
464
		);
465
		$this->assertTrue(
466
			$accountingmember->inGroup($staffgroup),
467
			'Users of child group are members of a direct parent group (if not in strict mode)'
468
		);
469
		$this->assertTrue(
470
			$ceomember->inGroup($staffgroup),
471
			'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
472
		);
473
		$this->assertTrue(
474
			$ceomember->inGroup($ceogroup, true),
475
			'Direct group membership is dected (if in strict mode)'
476
		);
477
		$this->assertFalse(
478
			$ceomember->inGroup($staffgroup, true),
479
			'Users of child group are not members of a direct parent group (if in strict mode)'
480
		);
481
		$this->assertFalse(
482
			$staffmember->inGroup($managementgroup),
483
			'Users of parent group are not members of a direct child group'
484
		);
485
		$this->assertFalse(
486
			$staffmember->inGroup($ceogroup),
487
			'Users of parent group are not members of an indirect grandchild group'
488
		);
489
		$this->assertFalse(
490
			$accountingmember->inGroup($managementgroup),
491
			'Users of group are not members of any siblings'
492
		);
493
		$this->assertFalse(
494
			$staffmember->inGroup('does-not-exist'),
495
			'Non-existant group returns false'
496
		);
497
	}
498
499
	/**
500
	 * Tests that the user is able to view their own record, and in turn, they can
501
	 * edit and delete their own record too.
502
	 */
503
	public function testCanManipulateOwnRecord() {
504
		$extensions = $this->removeExtensions(Object::get_extensions('Member'));
505
		$member = $this->objFromFixture('Member', 'test');
506
		$member2 = $this->objFromFixture('Member', 'staffmember');
507
508
		$this->session()->inst_set('loggedInAs', null);
509
510
		/* Not logged in, you can't view, delete or edit the record */
511
		$this->assertFalse($member->canView());
512
		$this->assertFalse($member->canDelete());
513
		$this->assertFalse($member->canEdit());
514
515
		/* Logged in users can edit their own record */
516
		$this->session()->inst_set('loggedInAs', $member->ID);
517
		$this->assertTrue($member->canView());
518
		$this->assertFalse($member->canDelete());
519
		$this->assertTrue($member->canEdit());
520
521
		/* Other uses cannot view, delete or edit others records */
522
		$this->session()->inst_set('loggedInAs', $member2->ID);
523
		$this->assertFalse($member->canView());
524
		$this->assertFalse($member->canDelete());
525
		$this->assertFalse($member->canEdit());
526
527
		$this->addExtensions($extensions);
528
		$this->session()->inst_set('loggedInAs', null);
529
	}
530
531
	public function testAuthorisedMembersCanManipulateOthersRecords() {
532
		$extensions = $this->removeExtensions(Object::get_extensions('Member'));
533
		$member = $this->objFromFixture('Member', 'test');
534
		$member2 = $this->objFromFixture('Member', 'staffmember');
535
536
		/* Group members with SecurityAdmin permissions can manipulate other records */
537
		$this->session()->inst_set('loggedInAs', $member->ID);
538
		$this->assertTrue($member2->canView());
539
		$this->assertTrue($member2->canDelete());
540
		$this->assertTrue($member2->canEdit());
541
542
		$this->addExtensions($extensions);
543
		$this->session()->inst_set('loggedInAs', null);
544
	}
545
546
	public function testExtendedCan() {
547
		$extensions = $this->removeExtensions(Object::get_extensions('Member'));
548
		$member = $this->objFromFixture('Member', 'test');
549
550
		/* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
551
		$this->assertFalse($member->canView());
552
		$this->assertFalse($member->canDelete());
553
		$this->assertFalse($member->canEdit());
554
555
		/* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
556
		Member::add_extension('MemberTest_ViewingAllowedExtension');
557
		$member2 = $this->objFromFixture('Member', 'staffmember');
558
559
		$this->assertTrue($member2->canView());
560
		$this->assertFalse($member2->canDelete());
561
		$this->assertFalse($member2->canEdit());
562
563
		/* Apply a extension that denies viewing of the Member */
564
		Member::remove_extension('MemberTest_ViewingAllowedExtension');
565
		Member::add_extension('MemberTest_ViewingDeniedExtension');
566
		$member3 = $this->objFromFixture('Member', 'managementmember');
567
568
		$this->assertFalse($member3->canView());
569
		$this->assertFalse($member3->canDelete());
570
		$this->assertFalse($member3->canEdit());
571
572
		/* Apply a extension that allows viewing and editing but denies deletion */
573
		Member::remove_extension('MemberTest_ViewingDeniedExtension');
574
		Member::add_extension('MemberTest_EditingAllowedDeletingDeniedExtension');
575
		$member4 = $this->objFromFixture('Member', 'accountingmember');
576
577
		$this->assertTrue($member4->canView());
578
		$this->assertFalse($member4->canDelete());
579
		$this->assertTrue($member4->canEdit());
580
581
		Member::remove_extension('MemberTest_EditingAllowedDeletingDeniedExtension');
582
		$this->addExtensions($extensions);
583
	}
584
585
	/**
586
	 * Tests for {@link Member::getName()} and {@link Member::setName()}
587
	 */
588
	public function testName() {
589
		$member = $this->objFromFixture('Member', 'test');
590
		$member->setName('Test Some User');
591
		$this->assertEquals('Test Some User', $member->getName());
592
		$member->setName('Test');
593
		$this->assertEquals('Test', $member->getName());
594
		$member->FirstName = 'Test';
0 ignored issues
show
Documentation introduced by
The property FirstName does not exist on object<SilverStripe\Model\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...
595
		$member->Surname = '';
0 ignored issues
show
Documentation introduced by
The property Surname does not exist on object<SilverStripe\Model\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...
596
		$this->assertEquals('Test', $member->getName());
597
	}
598
599
	public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves() {
600
		$adminMember = $this->objFromFixture('Member', 'admin');
601
		$otherAdminMember = $this->objFromFixture('Member', 'other-admin');
602
		$securityAdminMember = $this->objFromFixture('Member', 'test');
603
		$ceoMember = $this->objFromFixture('Member', 'ceomember');
604
605
		// Careful: Don't read as english language.
606
		// More precisely this should read canBeEditedBy()
607
608
		$this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
0 ignored issues
show
Bug introduced by
It seems like $adminMember defined by $this->objFromFixture('Member', 'admin') on line 600 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canEdit() does only seem to accept object<SilverStripe\Model\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...
609
		$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('Member', 'admin') on line 600 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canEdit() does only seem to accept object<SilverStripe\Model\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...
610
		$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('Member', 'admin') on line 600 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canEdit() does only seem to accept object<SilverStripe\Model\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...
611
612
		$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('Member', 'test') on line 602 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canEdit() does only seem to accept object<SilverStripe\Model\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...
613
		$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('Member', 'test') on line 602 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canEdit() does only seem to accept object<SilverStripe\Model\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...
614
		$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('Member', 'test') on line 602 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canEdit() does only seem to accept object<SilverStripe\Model\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...
615
	}
616
617
	public function testOnChangeGroups() {
618
		$staffGroup = $this->objFromFixture('Group', 'staffgroup');
619
		$staffMember = $this->objFromFixture('Member', 'staffmember');
620
		$adminMember = $this->objFromFixture('Member', 'admin');
621
		$newAdminGroup = new Group(array('Title' => 'newadmin'));
622
		$newAdminGroup->write();
623
		Permission::grant($newAdminGroup->ID, 'ADMIN');
624
		$newOtherGroup = new Group(array('Title' => 'othergroup'));
625
		$newOtherGroup->write();
626
627
		$this->assertTrue(
628
			$staffMember->onChangeGroups(array($staffGroup->ID)),
629
			'Adding existing non-admin group relation is allowed for non-admin members'
630
		);
631
		$this->assertTrue(
632
			$staffMember->onChangeGroups(array($newOtherGroup->ID)),
633
			'Adding new non-admin group relation is allowed for non-admin members'
634
		);
635
		$this->assertFalse(
636
			$staffMember->onChangeGroups(array($newAdminGroup->ID)),
637
			'Adding new admin group relation is not allowed for non-admin members'
638
		);
639
640
		$this->session()->inst_set('loggedInAs', $adminMember->ID);
641
		$this->assertTrue(
642
			$staffMember->onChangeGroups(array($newAdminGroup->ID)),
643
			'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
644
		);
645
		$this->session()->inst_set('loggedInAs', null);
646
647
		$this->assertTrue(
648
			$adminMember->onChangeGroups(array($newAdminGroup->ID)),
649
			'Adding new admin group relation is allowed for admin members'
650
		);
651
	}
652
653
	/**
654
	 * Test Member_GroupSet::add
655
	 */
656
	public function testOnChangeGroupsByAdd() {
657
		$staffMember = $this->objFromFixture('Member', 'staffmember');
658
		$adminMember = $this->objFromFixture('Member', 'admin');
659
660
		// Setup new admin group
661
		$newAdminGroup = new Group(array('Title' => 'newadmin'));
662
		$newAdminGroup->write();
663
		Permission::grant($newAdminGroup->ID, 'ADMIN');
664
665
		// Setup non-admin group
666
		$newOtherGroup = new Group(array('Title' => 'othergroup'));
667
		$newOtherGroup->write();
668
669
		// Test staff can be added to other group
670
		$this->assertFalse($staffMember->inGroup($newOtherGroup));
671
		$staffMember->Groups()->add($newOtherGroup);
672
		$this->assertTrue(
673
			$staffMember->inGroup($newOtherGroup),
674
			'Adding new non-admin group relation is allowed for non-admin members'
675
		);
676
677
		// Test staff member can't be added to admin groups
678
		$this->assertFalse($staffMember->inGroup($newAdminGroup));
679
		$staffMember->Groups()->add($newAdminGroup);
680
		$this->assertFalse(
681
			$staffMember->inGroup($newAdminGroup),
682
			'Adding new admin group relation is not allowed for non-admin members'
683
		);
684
685
		// Test staff member can be added to admin group by admins
686
		$this->logInAs($adminMember);
687
		$staffMember->Groups()->add($newAdminGroup);
688
		$this->assertTrue(
689
			$staffMember->inGroup($newAdminGroup),
690
			'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
691
		);
692
693
		// Test staff member can be added if they are already admin
694
		$this->session()->inst_set('loggedInAs', null);
695
		$this->assertFalse($adminMember->inGroup($newAdminGroup));
696
		$adminMember->Groups()->add($newAdminGroup);
697
		$this->assertTrue(
698
			$adminMember->inGroup($newAdminGroup),
699
			'Adding new admin group relation is allowed for admin members'
700
		);
701
	}
702
703
	/**
704
	 * Test Member_GroupSet::add
705
	 */
706
	public function testOnChangeGroupsBySetIDList() {
707
		$staffMember = $this->objFromFixture('Member', 'staffmember');
708
709
		// Setup new admin group
710
		$newAdminGroup = new Group(array('Title' => 'newadmin'));
711
		$newAdminGroup->write();
712
		Permission::grant($newAdminGroup->ID, 'ADMIN');
713
714
		// Test staff member can't be added to admin groups
715
		$this->assertFalse($staffMember->inGroup($newAdminGroup));
716
		$staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
717
		$this->assertFalse(
718
			$staffMember->inGroup($newAdminGroup),
719
			'Adding new admin group relation is not allowed for non-admin members'
720
		);
721
	}
722
723
	/**
724
	 * Test that extensions using updateCMSFields() are applied correctly
725
	 */
726
	public function testUpdateCMSFields() {
727
		Member::add_extension('MemberTest_FieldsExtension');
728
729
		$member = singleton('Member');
730
		$fields = $member->getCMSFields();
731
732
		$this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
733
		$this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
734
		$this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
735
736
		Member::remove_extension('MemberTest_FieldsExtension');
737
	}
738
739
	/**
740
	 * Test that all members are returned
741
	 */
742
	public function testMap_in_groupsReturnsAll() {
743
		$members = Member::map_in_groups();
744
		$this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
745
	}
746
747
	/**
748
	 * Test that only admin members are returned
749
	 */
750
	public function testMap_in_groupsReturnsAdmins() {
751
		$adminID = $this->objFromFixture('Group', 'admingroup')->ID;
752
		$members = Member::map_in_groups($adminID)->toArray();
753
754
		$admin = $this->objFromFixture('Member', 'admin');
755
		$otherAdmin = $this->objFromFixture('Member', 'other-admin');
756
757
		$this->assertTrue(in_array($admin->getTitle(), $members),
758
			$admin->getTitle().' should be in the returned list.');
759
		$this->assertTrue(in_array($otherAdmin->getTitle(), $members),
760
			$otherAdmin->getTitle().' should be in the returned list.');
761
		$this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
762
	}
763
764
	/**
765
	 * Add the given array of member extensions as class names.
766
	 * This is useful for re-adding extensions after being removed
767
	 * in a test case to produce an unbiased test.
768
	 *
769
	 * @param array $extensions
770
	 * @return array The added extensions
771
	 */
772
	protected function addExtensions($extensions) {
773
		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...
774
			Member::add_extension($extension);
775
		}
776
		return $extensions;
777
	}
778
779
	/**
780
	 * Remove given extensions from Member. This is useful for
781
	 * removing extensions that could produce a biased
782
	 * test result, as some extensions applied by project
783
	 * code or modules can do this.
784
	 *
785
	 * @param array $extensions
786
	 * @return array The removed extensions
787
	 */
788
	protected function removeExtensions($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::remove_extension($extension);
791
		}
792
		return $extensions;
793
	}
794
795
	public function testGenerateAutologinTokenAndStoreHash() {
796
		$enc = new PasswordEncryptor_Blowfish();
797
798
		$m = new Member();
799
		$m->PasswordEncryption = 'blowfish';
800
		$m->Salt = $enc->salt('123');
801
802
		$token = $m->generateAutologinTokenAndStoreHash();
803
804
		$this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as ahash.');
805
	}
806
807
	public function testValidateAutoLoginToken() {
808
		$enc = new PasswordEncryptor_Blowfish();
809
810
		$m1 = new Member();
811
		$m1->PasswordEncryption = 'blowfish';
812
		$m1->Salt = $enc->salt('123');
813
		$m1Token = $m1->generateAutologinTokenAndStoreHash();
814
815
		$m2 = new Member();
816
		$m2->PasswordEncryption = 'blowfish';
817
		$m2->Salt = $enc->salt('456');
818
		$m2Token = $m2->generateAutologinTokenAndStoreHash();
0 ignored issues
show
Unused Code introduced by
$m2Token is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
819
820
		$this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
821
		$this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
822
	}
823
824
	public function testRememberMeHashGeneration() {
825
		$m1 = $this->objFromFixture('Member', 'grouplessmember');
826
827
		$m1->login(true);
828
		$hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
829
		$this->assertEquals($hashes->Count(), 1);
830
		$firstHash = $hashes->First();
831
		$this->assertNotNull($firstHash->DeviceID);
832
		$this->assertNotNull($firstHash->Hash);
833
	}
834
835
	public function testRememberMeHashAutologin() {
836
		$m1 = $this->objFromFixture('Member', 'noexpiry');
837
838
		$m1->login(true);
839
		$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
840
		$this->assertNotNull($firstHash);
841
842
		// re-generates the hash so we can get the token
843
		$firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash does not exist on object<SilverStripe\Model\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...
844
		$token = $firstHash->getToken();
845
		$firstHash->write();
846
847
		$response = $this->get(
848
			'Security/login',
849
			$this->session(),
850
			null,
851
			array(
852
				'alc_enc' => $m1->ID.':'.$token,
853
				'alc_device' => $firstHash->DeviceID
854
			)
855
		);
856
		$message = _t(
857
			'Member.LOGGEDINAS',
858
			"You're logged in as {name}.",
859
			array('name' => $m1->FirstName)
0 ignored issues
show
Documentation introduced by
array('name' => $m1->FirstName) is of type array<string,?,{"name":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
860
		);
861
		$this->assertContains($message, $response->getBody());
862
863
		$this->session()->inst_set('loggedInAs', null);
864
865
		// A wrong token or a wrong device ID should not let us autologin
866
		$response = $this->get(
867
			'Security/login',
868
			$this->session(),
869
			null,
870
			array(
871
				'alc_enc' => $m1->ID.':'.str_rot13($token),
872
				'alc_device' => $firstHash->DeviceID
873
			)
874
		);
875
		$this->assertNotContains($message, $response->getBody());
876
877
		$response = $this->get(
878
			'Security/login',
879
			$this->session(),
880
			null,
881
			array(
882
				'alc_enc' => $m1->ID.':'.$token,
883
				'alc_device' => str_rot13($firstHash->DeviceID)
884
			)
885
		);
886
		$this->assertNotContains($message, $response->getBody());
887
888
		// Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
889
		// should remove all previous hashes for this device
890
		$response = $this->post(
891
			'Security/LoginForm',
892
			array(
893
				'Email' => $m1->Email,
894
				'Password' => '1nitialPassword',
895
				'AuthenticationMethod' => 'MemberAuthenticator',
896
				'action_dologin' => 'action_dologin'
897
			),
898
			null,
899
			$this->session(),
900
			null,
901
			array(
902
				'alc_device' => $firstHash->DeviceID
903
			)
904
		);
905
		$this->assertContains($message, $response->getBody());
906
		$this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->Count(), 0);
907
	}
908
909
	public function testExpiredRememberMeHashAutologin() {
910
		$m1 = $this->objFromFixture('Member', 'noexpiry');
911
912
		$m1->login(true);
913
		$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
914
		$this->assertNotNull($firstHash);
915
916
		// re-generates the hash so we can get the token
917
		$firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash does not exist on object<SilverStripe\Model\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...
918
		$token = $firstHash->getToken();
919
		$firstHash->ExpiryDate = '2000-01-01 00:00:00';
0 ignored issues
show
Documentation introduced by
The property ExpiryDate does not exist on object<SilverStripe\Model\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...
920
		$firstHash->write();
921
922
		SS_DateTime::set_mock_now('1999-12-31 23:59:59');
923
924
		$response = $this->get(
925
			'Security/login',
926
			$this->session(),
927
			null,
928
			array(
929
				'alc_enc' => $m1->ID.':'.$token,
930
				'alc_device' => $firstHash->DeviceID
931
			)
932
		);
933
		$message = _t(
934
			'Member.LOGGEDINAS',
935
			"You're logged in as {name}.",
936
			array('name' => $m1->FirstName)
0 ignored issues
show
Documentation introduced by
array('name' => $m1->FirstName) is of type array<string,?,{"name":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
937
		);
938
		$this->assertContains($message, $response->getBody());
939
940
		$this->session()->inst_set('loggedInAs', null);
941
942
		// re-generates the hash so we can get the token
943
		$firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash does not exist on object<SilverStripe\Model\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...
944
		$token = $firstHash->getToken();
945
		$firstHash->ExpiryDate = '2000-01-01 00:00:00';
0 ignored issues
show
Documentation introduced by
The property ExpiryDate does not exist on object<SilverStripe\Model\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...
946
		$firstHash->write();
947
948
		SS_DateTime::set_mock_now('2000-01-01 00:00:01');
949
950
		$response = $this->get(
951
			'Security/login',
952
			$this->session(),
953
			null,
954
			array(
955
				'alc_enc' => $m1->ID.':'.$token,
956
				'alc_device' => $firstHash->DeviceID
957
			)
958
		);
959
		$this->assertNotContains($message, $response->getBody());
960
		$this->session()->inst_set('loggedInAs', null);
961
		SS_Datetime::clear_mock_now();
962
	}
963
964
	public function testRememberMeMultipleDevices() {
965
		$m1 = $this->objFromFixture('Member', 'noexpiry');
966
967
		// First device
968
		$m1->login(true);
969
		Cookie::set('alc_device', null);
970
		// Second device
971
		$m1->login(true);
972
973
		// Hash of first device
974
		$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
975
		$this->assertNotNull($firstHash);
976
977
		// Hash of second device
978
		$secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->Last();
979
		$this->assertNotNull($secondHash);
980
981
		// DeviceIDs are different
982
		$this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
983
984
		// re-generates the hashes so we can get the tokens
985
		$firstHash->Hash = $firstHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash does not exist on object<SilverStripe\Model\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...
986
		$firstToken = $firstHash->getToken();
987
		$firstHash->write();
988
989
		$secondHash->Hash = $secondHash->getNewHash($m1);
0 ignored issues
show
Documentation introduced by
The property Hash does not exist on object<SilverStripe\Model\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...
990
		$secondToken = $secondHash->getToken();
991
		$secondHash->write();
992
993
		// Accessing the login page should show the user's name straight away
994
		$response = $this->get(
995
			'Security/login',
996
			$this->session(),
997
			null,
998
			array(
999
				'alc_enc' => $m1->ID.':'.$firstToken,
1000
				'alc_device' => $firstHash->DeviceID
1001
			)
1002
		);
1003
		$message = _t(
1004
			'Member.LOGGEDINAS',
1005
			"You're logged in as {name}.",
1006
			array('name' => $m1->FirstName)
0 ignored issues
show
Documentation introduced by
array('name' => $m1->FirstName) is of type array<string,?,{"name":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1007
		);
1008
		$this->assertContains($message, $response->getBody());
1009
1010
		$this->session()->inst_set('loggedInAs', null);
1011
1012
		// Accessing the login page from the second device
1013
		$response = $this->get(
1014
			'Security/login',
1015
			$this->session(),
1016
			null,
1017
			array(
1018
				'alc_enc' => $m1->ID.':'.$secondToken,
1019
				'alc_device' => $secondHash->DeviceID
1020
			)
1021
		);
1022
		$this->assertContains($message, $response->getBody());
1023
1024
		$logout_across_devices = Config::inst()->get('RememberLoginHash', 'logout_across_devices');
1025
1026
		// Logging out from the second device - only one device being logged out
1027
		Config::inst()->update('RememberLoginHash', 'logout_across_devices', false);
1028
		$response = $this->get(
0 ignored issues
show
Unused Code introduced by
$response is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1029
			'Security/logout',
1030
			$this->session(),
1031
			null,
1032
			array(
1033
				'alc_enc' => $m1->ID.':'.$secondToken,
1034
				'alc_device' => $secondHash->DeviceID
1035
			)
1036
		);
1037
		$this->assertEquals(
1038
			RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->Count(),
1039
			1
1040
		);
1041
1042
		// Logging out from any device when all login hashes should be removed
1043
		Config::inst()->update('RememberLoginHash', 'logout_across_devices', true);
1044
		$m1->login(true);
1045
		$response = $this->get('Security/logout', $this->session());
0 ignored issues
show
Unused Code introduced by
$response is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1046
		$this->assertEquals(
1047
			RememberLoginHash::get()->filter('MemberID', $m1->ID)->Count(),
1048
			0
1049
		);
1050
1051
		Config::inst()->update('RememberLoginHash', 'logout_across_devices', $logout_across_devices);
1052
	}
1053
1054
	public function testCanDelete() {
1055
		$admin1 = $this->objFromFixture('Member', 'admin');
1056
		$admin2 = $this->objFromFixture('Member', 'other-admin');
1057
		$member1 = $this->objFromFixture('Member', 'grouplessmember');
1058
		$member2 = $this->objFromFixture('Member', 'noformatmember');
1059
1060
		$this->assertTrue(
1061
			$admin1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture('Member', 'other-admin') on line 1056 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canDelete() does only seem to accept object<SilverStripe\Model\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...
1062
			'Admins can delete other admins'
1063
		);
1064
		$this->assertTrue(
1065
			$member1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture('Member', 'other-admin') on line 1056 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canDelete() does only seem to accept object<SilverStripe\Model\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...
1066
			'Admins can delete non-admins'
1067
		);
1068
		$this->assertFalse(
1069
			$admin1->canDelete($admin1),
0 ignored issues
show
Bug introduced by
It seems like $admin1 defined by $this->objFromFixture('Member', 'admin') on line 1055 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canDelete() does only seem to accept object<SilverStripe\Model\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...
1070
			'Admins can not delete themselves'
1071
		);
1072
		$this->assertFalse(
1073
			$member1->canDelete($member2),
0 ignored issues
show
Bug introduced by
It seems like $member2 defined by $this->objFromFixture('Member', 'noformatmember') on line 1058 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canDelete() does only seem to accept object<SilverStripe\Model\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...
1074
			'Non-admins can not delete other non-admins'
1075
		);
1076
		$this->assertFalse(
1077
			$member1->canDelete($member1),
0 ignored issues
show
Bug introduced by
It seems like $member1 defined by $this->objFromFixture('M...er', 'grouplessmember') on line 1057 can also be of type object<SilverStripe\Model\DataObject>; however, SilverStripe\Model\DataObject::canDelete() does only seem to accept object<SilverStripe\Model\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...
1078
			'Non-admins can not delete themselves'
1079
		);
1080
	}
1081
1082
	public function testFailedLoginCount() {
1083
		$maxFailedLoginsAllowed = 3;
1084
		//set up the config variables to enable login lockouts
1085
		Config::nest();
1086
		Config::inst()->update('Member', 'lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
1087
1088
		$member = $this->objFromFixture('Member', 'test');
1089
		$failedLoginCount = $member->FailedLoginCount;
0 ignored issues
show
Documentation introduced by
The property FailedLoginCount does not exist on object<SilverStripe\Model\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...
1090
1091
		for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
1092
			$member->registerFailedLogin();
1093
1094
			$this->assertEquals(
1095
				++$failedLoginCount,
1096
				$member->FailedLoginCount,
1097
				'Failed to increment $member->FailedLoginCount'
1098
			);
1099
1100
			$this->assertFalse(
1101
				$member->isLockedOut(),
1102
				"Member has been locked out too early"
1103
			);
1104
		}
1105
	}
1106
1107
	public function testMemberValidator()
1108
	{
1109
		// clear custom requirements for this test
1110
		Config::inst()->update('Member_Validator', 'customRequired', null);
1111
		$memberA = $this->objFromFixture('Member', 'admin');
1112
		$memberB = $this->objFromFixture('Member', 'test');
1113
1114
		// create a blank form
1115
		$form = new MemberTest_ValidatorForm();
1116
1117
		$validator = new Member_Validator();
1118
		$validator->setForm($form);
1119
1120
		// Simulate creation of a new member via form, but use an existing member identifier
1121
		$fail = $validator->php(array(
1122
			'FirstName' => 'Test',
1123
			'Email' => $memberA->Email
1124
		));
1125
1126
		$this->assertFalse(
1127
			$fail,
1128
			'Member_Validator must fail when trying to create new Member with existing Email.'
1129
		);
1130
1131
		// populate the form with values from another member
1132
		$form->loadDataFrom($memberB);
0 ignored issues
show
Documentation introduced by
$memberB is of type object<SilverStripe\Model\DataObject>|null, but the function expects a array|object<DataObject>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1133
1134
		// Assign the validator to an existing member
1135
		// (this is basically the same as passing the member ID with the form data)
1136
		$validator->setForMember($memberB);
0 ignored issues
show
Documentation introduced by
$memberB is of type object<SilverStripe\Model\DataObject>|null, but the function expects a object<Member>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1137
1138
		// Simulate update of a member via form and use an existing member Email
1139
		$fail = $validator->php(array(
1140
			'FirstName' => 'Test',
1141
			'Email' => $memberA->Email
1142
		));
1143
1144
		// Simulate update to a new Email address
1145
		$pass1 = $validator->php(array(
1146
			'FirstName' => 'Test',
1147
			'Email' => '[email protected]'
1148
		));
1149
1150
		// Pass in the same Email address that the member already has. Ensure that case is valid
1151
		$pass2 = $validator->php(array(
1152
			'FirstName' => 'Test',
1153
			'Surname' => 'User',
1154
			'Email' => $memberB->Email
1155
		));
1156
1157
		$this->assertFalse(
1158
			$fail,
1159
			'Member_Validator must fail when trying to update existing member with existing Email.'
1160
		);
1161
1162
		$this->assertTrue(
1163
			$pass1,
1164
			'Member_Validator must pass when Email is updated to a value that\'s not in use.'
1165
		);
1166
1167
		$this->assertTrue(
1168
			$pass2,
1169
			'Member_Validator must pass when Member updates his own Email to the already existing value.'
1170
		);
1171
	}
1172
1173
	public function testMemberValidatorWithExtensions()
1174
	{
1175
		// clear custom requirements for this test
1176
		Config::inst()->update('Member_Validator', 'customRequired', null);
1177
1178
		// create a blank form
1179
		$form = new MemberTest_ValidatorForm();
1180
1181
		// Test extensions
1182
		Member_Validator::add_extension('MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension');
1183
		$validator = new Member_Validator();
1184
		$validator->setForm($form);
1185
1186
		// This test should fail, since the extension enforces FirstName == Surname
1187
		$fail = $validator->php(array(
1188
			'FirstName' => 'Test',
1189
			'Surname' => 'User',
1190
			'Email' => '[email protected]'
1191
		));
1192
1193
		$pass = $validator->php(array(
1194
			'FirstName' => 'Test',
1195
			'Surname' => 'Test',
1196
			'Email' => '[email protected]'
1197
		));
1198
1199
		$this->assertFalse(
1200
			$fail,
1201
			'Member_Validator must fail because of added extension.'
1202
		);
1203
1204
		$this->assertTrue(
1205
			$pass,
1206
			'Member_Validator must succeed, since it meets all requirements.'
1207
		);
1208
1209
		// Add another extension that always fails. This ensures that all extensions are considered in the validation
1210
		Member_Validator::add_extension('MemberTest_MemberValidator_AlwaysFailsExtension');
1211
		$validator = new Member_Validator();
1212
		$validator->setForm($form);
1213
1214
		// Even though the data is valid, This test should still fail, since one extension always returns false
1215
		$fail = $validator->php(array(
1216
			'FirstName' => 'Test',
1217
			'Surname' => 'Test',
1218
			'Email' => '[email protected]'
1219
		));
1220
1221
		$this->assertFalse(
1222
			$fail,
1223
			'Member_Validator must fail because of added extensions.'
1224
		);
1225
1226
		// Remove added extensions
1227
		Member_Validator::remove_extension('MemberTest_MemberValidator_AlwaysFailsExtension');
1228
		Member_Validator::remove_extension('MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension');
1229
	}
1230
1231
	public function testCustomMemberValidator()
1232
	{
1233
		// clear custom requirements for this test
1234
		Config::inst()->update('Member_Validator', 'customRequired', null);
1235
1236
		$member = $this->objFromFixture('Member', 'admin');
1237
1238
		$form = new MemberTest_ValidatorForm();
1239
		$form->loadDataFrom($member);
0 ignored issues
show
Documentation introduced by
$member is of type object<SilverStripe\Model\DataObject>|null, but the function expects a array|object<DataObject>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1240
1241
		$validator = new Member_Validator();
1242
		$validator->setForm($form);
1243
1244
		$pass = $validator->php(array(
1245
			'FirstName' => 'Borris',
1246
			'Email' => '[email protected]'
1247
		));
1248
1249
		$fail = $validator->php(array(
1250
			'Email' => '[email protected]',
1251
			'Surname' => ''
1252
		));
1253
1254
		$this->assertTrue($pass, 'Validator requires a FirstName and Email');
1255
		$this->assertFalse($fail, 'Missing FirstName');
1256
1257
		$ext = new MemberTest_ValidatorExtension();
1258
		$ext->updateValidator($validator);
1259
1260
		$pass = $validator->php(array(
1261
			'FirstName' => 'Borris',
1262
			'Email' => '[email protected]'
1263
		));
1264
1265
		$fail = $validator->php(array(
1266
			'Email' => '[email protected]'
1267
		));
1268
1269
		$this->assertFalse($pass, 'Missing surname');
1270
		$this->assertFalse($fail, 'Missing surname value');
1271
1272
		$fail = $validator->php(array(
1273
			'Email' => '[email protected]',
1274
			'Surname' => 'Silverman'
1275
		));
1276
1277
		$this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1278
	}
1279
1280
}
1281
1282
/**
1283
 * @package framework
1284
 * @subpackage tests
1285
 */
1286
class MemberTest_ValidatorForm extends Form implements TestOnly {
1287
1288
	public function __construct() {
1289
		parent::__construct(Controller::curr(), __CLASS__, new FieldList(
1290
			new TextField('Email'),
1291
			new TextField('Surname'),
1292
			new TextField('ID'),
1293
			new TextField('FirstName')
1294
		), new FieldList(
1295
			new FormAction('someAction')
1296
		));
1297
	}
1298
}
1299
1300
/**
1301
 * @package framework
1302
 * @subpackage tests
1303
 */
1304
class MemberTest_ValidatorExtension extends DataExtension implements TestOnly {
1305
1306
	public function updateValidator(&$validator) {
1307
		$validator->addRequiredField('Surname');
1308
		$validator->removeRequiredField('FirstName');
1309
	}
1310
}
1311
1312
/**
1313
 * Extension that adds additional validation criteria
1314
 * @package framework
1315
 * @subpackage tests
1316
 */
1317
class MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension extends DataExtension implements TestOnly
1318
{
1319
	public function updatePHP($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1320
		return $data['FirstName'] == $data['Surname'];
1321
	}
1322
}
1323
1324
/**
1325
 * Extension that adds additional validation criteria
1326
 * @package framework
1327
 * @subpackage tests
1328
 */
1329
class MemberTest_MemberValidator_AlwaysFailsExtension extends DataExtension implements TestOnly
1330
{
1331
	public function updatePHP($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1332
		return false;
1333
	}
1334
}
1335
1336
/**
1337
 * @package framework
1338
 * @subpackage tests
1339
 */
1340
class MemberTest_ViewingAllowedExtension extends DataExtension implements TestOnly {
1341
1342
	public function canView($member = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $member is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1343
		return true;
1344
	}
1345
}
1346
1347
/**
1348
 * @package framework
1349
 * @subpackage tests
1350
 */
1351
class MemberTest_ViewingDeniedExtension extends DataExtension implements TestOnly {
1352
1353
	public function canView($member = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $member is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1354
		return false;
1355
	}
1356
}
1357
1358
/**
1359
 * @package framework
1360
 * @subpackage tests
1361
 */
1362
class MemberTest_FieldsExtension extends DataExtension implements TestOnly {
1363
1364
	public function updateCMSFields(FieldList $fields) {
1365
		$fields->addFieldToTab('Root.Main', new TextField('TestMemberField', 'Test'));
1366
	}
1367
1368
}
1369
1370
/**
1371
 * @package framework
1372
 * @subpackage tests
1373
 */
1374
class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension implements TestOnly {
1375
1376
	public function canView($member = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $member is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1377
		return true;
1378
	}
1379
1380
	public function canEdit($member = null) {
1381
		return true;
1382
	}
1383
1384
	public function canDelete($member = null) {
1385
		return false;
1386
	}
1387
1388
}
1389
1390
/**
1391
 * @package framework
1392
 * @subpackage tests
1393
 */
1394
class MemberTest_PasswordValidator extends PasswordValidator {
1395
	public function __construct() {
1396
		parent::__construct();
1397
		$this->minLength(7);
1398
		$this->checkHistoricalPasswords(6);
1399
		$this->characterStrength(3, array('lowercase','uppercase','digits','punctuation'));
1400
	}
1401
1402
}
1403