Completed
Push — create-endpoint-fetcher ( 23bf41 )
by Sam
07:40
created

MemberTest::testForgotPasswordEmaling()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1236
1237
		$validator = new Member_Validator();
1238
		$validator->setForm($form);
1239
1240
		$pass = $validator->php(array(
1241
			'FirstName' => 'Borris',
1242
			'Email' => '[email protected]'
1243
		));
1244
1245
		$fail = $validator->php(array(
1246
			'Email' => '[email protected]',
1247
			'Surname' => ''
1248
		));
1249
1250
		$this->assertTrue($pass, 'Validator requires a FirstName and Email');
1251
		$this->assertFalse($fail, 'Missing FirstName');
1252
1253
		$ext = new MemberTest_ValidatorExtension();
1254
		$ext->updateValidator($validator);
1255
1256
		$pass = $validator->php(array(
1257
			'FirstName' => 'Borris',
1258
			'Email' => '[email protected]'
1259
		));
1260
1261
		$fail = $validator->php(array(
1262
			'Email' => '[email protected]'
1263
		));
1264
1265
		$this->assertFalse($pass, 'Missing surname');
1266
		$this->assertFalse($fail, 'Missing surname value');
1267
1268
		$fail = $validator->php(array(
1269
			'Email' => '[email protected]',
1270
			'Surname' => 'Silverman'
1271
		));
1272
1273
		$this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1274
	}
1275
1276
}
1277
1278
/**
1279
 * @package framework
1280
 * @subpackage tests
1281
 */
1282
class MemberTest_ValidatorForm extends Form implements TestOnly {
1283
1284
	public function __construct() {
1285
		parent::__construct(Controller::curr(), __CLASS__, new FieldList(
1286
			new TextField('Email'),
1287
			new TextField('Surname'),
1288
			new TextField('ID'),
1289
			new TextField('FirstName')
1290
		), new FieldList(
1291
			new FormAction('someAction')
1292
		));
1293
	}
1294
}
1295
1296
/**
1297
 * @package framework
1298
 * @subpackage tests
1299
 */
1300
class MemberTest_ValidatorExtension extends DataExtension implements TestOnly {
1301
1302
	public function updateValidator(&$validator) {
1303
		$validator->addRequiredField('Surname');
1304
		$validator->removeRequiredField('FirstName');
1305
	}
1306
}
1307
1308
/**
1309
 * Extension that adds additional validation criteria
1310
 * @package framework
1311
 * @subpackage tests
1312
 */
1313
class MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension extends DataExtension implements TestOnly
1314
{
1315
	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...
1316
		return $data['FirstName'] == $data['Surname'];
1317
	}
1318
}
1319
1320
/**
1321
 * Extension that adds additional validation criteria
1322
 * @package framework
1323
 * @subpackage tests
1324
 */
1325
class MemberTest_MemberValidator_AlwaysFailsExtension extends DataExtension implements TestOnly
1326
{
1327
	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...
1328
		return false;
1329
	}
1330
}
1331
1332
/**
1333
 * @package framework
1334
 * @subpackage tests
1335
 */
1336
class MemberTest_ViewingAllowedExtension extends DataExtension implements TestOnly {
1337
1338
	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...
1339
		return true;
1340
	}
1341
}
1342
1343
/**
1344
 * @package framework
1345
 * @subpackage tests
1346
 */
1347
class MemberTest_ViewingDeniedExtension extends DataExtension implements TestOnly {
1348
1349
	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...
1350
		return false;
1351
	}
1352
}
1353
1354
/**
1355
 * @package framework
1356
 * @subpackage tests
1357
 */
1358
class MemberTest_FieldsExtension extends DataExtension implements TestOnly {
1359
1360
	public function updateCMSFields(FieldList $fields) {
1361
		$fields->addFieldToTab('Root.Main', new TextField('TestMemberField', 'Test'));
1362
	}
1363
1364
}
1365
1366
/**
1367
 * @package framework
1368
 * @subpackage tests
1369
 */
1370
class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension implements TestOnly {
1371
1372
	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...
1373
		return true;
1374
	}
1375
1376
	public function canEdit($member = null) {
1377
		return true;
1378
	}
1379
1380
	public function canDelete($member = null) {
1381
		return false;
1382
	}
1383
1384
}
1385
1386
/**
1387
 * @package framework
1388
 * @subpackage tests
1389
 */
1390
class MemberTest_PasswordValidator extends PasswordValidator {
1391
	public function __construct() {
1392
		parent::__construct();
1393
		$this->minLength(7);
1394
		$this->checkHistoricalPasswords(6);
1395
		$this->characterStrength(3, array('lowercase','uppercase','digits','punctuation'));
1396
	}
1397
1398
}
1399