Merge remote-tracking branch 'origin/4.0.4' into 4.1.1

# Conflicts:
  #	src/Control/Director.php
This commit is contained in:
Robbie Averill 2018-05-24 15:41:11 +12:00
commit 722202fef4
20 changed files with 348 additions and 59 deletions

View File

@ -348,6 +348,24 @@ RewriteRule .* ../index.php [QSA]
You will need to ensure that your core apache configuration has the necessary `AllowOverride` You will need to ensure that your core apache configuration has the necessary `AllowOverride`
settings to support the local .htaccess file. settings to support the local .htaccess file.
Although assets have a 404 handler which routes to a PHP handler, .php files within assets itself
should not be allowed to be marked as executable.
When securing your server you should ensure that you protect against both files that can be uploaded as
executable on the server, as well as protect against accidental upload of `.htaccess` which bypasses
this file security.
For instance your server configuration should look similar to the below:
```
<Directory "/var/www/superarcade/public/assets">
php_admin_flag engine off
</Directory>
```
The `php_admin_flag` will protect against uploaded `.htaccess` files accidentally re-enabling script
execution within the assets directory.
#### Configuring Web Server: Windows IIS 7.5+ #### Configuring Web Server: Windows IIS 7.5+
Configuring via IIS requires the Rewrite extension to be installed and configured properly. Configuring via IIS requires the Rewrite extension to be installed and configured properly.

View File

@ -0,0 +1,32 @@
# 4.0.4
This security release removes the following file extensions from the default whitelist of accepted types for
uploaded files: `dotm`, `potm`, `jar`, `css`, `js` and `xltm`.
If you require the ability to upload these file types in your projects, you will need to add them back in again.
For more information, see ["Configuring: File types"](https://docs.silverstripe.org/en/4/developer_guides/files/file_security/#configuring-file-types).
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-04-26 [299131ed2](https://github.com/silverstripe/silverstripe-framework/commit/299131ed2) File security documentation (Damian Mooyman) - See [ss-2018-012](http://www.silverstripe.org/download/security-releases/ss-2018-012)
* 2018-04-25 [be96858](https://github.com/silverstripe/silverstripe-installer/commit/be96858e85272ca62f6f0ff3e24a44aa0248ac4d) Remove jar, dotm, potm, xltm from file extension whitelist, hard-code CSS and JS for TinyMCE support (Robbie Averill) - See [ss-2018-014](http://www.silverstripe.org/download/security-releases/ss-2018-014)
* 2018-04-24 [f847f186b](https://github.com/silverstripe/silverstripe-framework/commit/f847f186b) Remove password text from session data on failed submission (Aaron Carlino) - See [ss-2018-013](http://www.silverstripe.org/download/security-releases/ss-2018-013)
* 2018-04-23 [aa365e0](https://github.com/silverstripe/silverstripe-assets/commit/aa365e0) Remove dotm, potm, jar, css, js, xltm from default File.allowed_extensions (Robbie Averill) - See [ss-2018-014](http://www.silverstripe.org/download/security-releases/ss-2018-014)
* 2018-04-23 [f9c03fa](https://github.com/silverstripe/silverstripe-installer/commit/f9c03fa623dc7237005901efd863256b7d356db7) Prevent php code execution in assets folder (Damian Mooyman) - See [ss-2018-012](http://www.silverstripe.org/download/security-releases/ss-2018-012)
* 2018-04-23 [1e27835](https://github.com/silverstripe/silverstripe-assets/commit/1e27835) Prevent php code execution in assets folder (Damian Mooyman) - See [ss-2018-012](http://www.silverstripe.org/download/security-releases/ss-2018-012)
* 2018-04-22 [beec0c0d4](https://github.com/silverstripe/silverstripe-framework/commit/beec0c0d4) regression of SS-2017-002 (Robbie Averill) - See [ss-2018-010](http://www.silverstripe.org/download/security-releases/ss-2018-010)
* 2018-04-11 [e409d6f67](https://github.com/silverstripe/silverstripe-framework/commit/e409d6f67) Restrict non-admins from being assigned to admin groups (Damian Mooyman) - See [ss-2018-001](http://www.silverstripe.org/download/security-releases/ss-2018-001)
* 2018-04-10 [9053014a7](https://github.com/silverstripe/silverstripe-framework/commit/9053014a7) Validate against malformed urls (Damian Mooyman) - See [ss-2018-008](http://www.silverstripe.org/download/security-releases/ss-2018-008)
* 2018-04-10 [2e13ae746](https://github.com/silverstripe/silverstripe-framework/commit/2e13ae746) Prevent code execution in template value resolution (Damian Mooyman) - See [ss-2018-006](http://www.silverstripe.org/download/security-releases/ss-2018-006)
* 2018-04-09 [db04ed9](https://github.com/silverstripe/silverstripe-admin/commit/db04ed9) Remove on* events as allowed properties (Damian Mooyman) - See [ss-2018-004](http://www.silverstripe.org/download/security-releases/ss-2018-004)
* 2018-04-08 [d935140a9](https://github.com/silverstripe/silverstripe-framework/commit/d935140a9) Prevent unauthenticated isDev / isTest being allowed (Damian Mooyman) - See [ss-2018-005](http://www.silverstripe.org/download/security-releases/ss-2018-005)
### Bugfixes
* 2018-05-23 [e7e32d13a](https://github.com/silverstripe/silverstripe-framework/commit/e7e32d13a) Add namespace and encryptor to tests that expect blowfish to be available (Robbie Averill)
* 2018-02-13 [c6095cf](https://github.com/silverstripe/silverstripe-config/commit/c6095cfc0a07a74bb932e2191215d06f102e992a) word boundary issue with pathname matching (Christopher Joe)
* 2018-02-06 [5bff64b47](https://github.com/silverstripe/silverstripe-framework/commit/5bff64b47) Fix Director::test() not persisting removed session keys on teardown (Damian Mooyman)

View File

@ -473,6 +473,27 @@ class Director implements TemplateGlobalProvider
return $host; return $host;
} }
/**
* Validate user and password in URL, disallowing slashes
*
* @param string $url
* @return bool
*/
protected static function validateUserAndPass($url)
{
$parsedURL = parse_url($url);
// Validate user (disallow slashes)
if (!empty($parsedURL['user']) && strstr($parsedURL['user'], '\\')) {
return false;
}
if (!empty($parsedURL['pass']) && strstr($parsedURL['pass'], '\\')) {
return false;
}
return true;
}
/** /**
* A helper to determine the current hostname used to access the site. * A helper to determine the current hostname used to access the site.
* The following are used to determine the host (in order) * The following are used to determine the host (in order)
@ -811,6 +832,11 @@ class Director implements TemplateGlobalProvider
*/ */
public static function is_site_url($url) public static function is_site_url($url)
{ {
// Validate user and password
if (!static::validateUserAndPass($url)) {
return false;
}
// Validate host[:port] // Validate host[:port]
$urlHost = static::parseHost($url); $urlHost = static::parseHost($url);
if ($urlHost && $urlHost === static::host()) { if ($urlHost && $urlHost === static::host()) {

View File

@ -214,6 +214,7 @@ class ParameterConfirmationToken
*/ */
public function suppress() public function suppress()
{ {
unset($_GET[$this->parameterName]);
$this->request->offsetUnset($this->parameterName); $this->request->offsetUnset($this->parameterName);
} }

View File

@ -121,7 +121,7 @@ class FixtureBlueprint
continue; continue;
} }
if (is_callable($fieldVal)) { if (!is_string($fieldVal) && is_callable($fieldVal)) {
$obj->$fieldName = $fieldVal($obj, $data, $fixtures); $obj->$fieldName = $fieldVal($obj, $data, $fixtures);
} else { } else {
$obj->$fieldName = $fieldVal; $obj->$fieldName = $fieldVal;

View File

@ -157,7 +157,6 @@ class FormRequestHandler extends RequestHandler
"SilverStripe\\Forms\\Form.CSRF_EXPIRED_MESSAGE", "SilverStripe\\Forms\\Form.CSRF_EXPIRED_MESSAGE",
"Your session has expired. Please re-submit the form." "Your session has expired. Please re-submit the form."
)); ));
// Return the user // Return the user
return $this->redirectBack(); return $this->redirectBack();
} }

View File

@ -281,7 +281,7 @@ class GridFieldDataColumns implements GridField_ColumnProvider
} }
$spec = $this->fieldFormatting[$fieldName]; $spec = $this->fieldFormatting[$fieldName];
if (is_callable($spec)) { if (!is_string($spec) && is_callable($spec)) {
return $spec($value, $item); return $spec($value, $item);
} else { } else {
$format = str_replace('$value', "__VAL__", $spec); $format = str_replace('$value', "__VAL__", $spec);

View File

@ -19,6 +19,12 @@ class PasswordField extends TextField
protected $inputType = 'password'; protected $inputType = 'password';
/**
* If true, the field can accept a value attribute, e.g. from posted form data
* @var bool
*/
protected $allowValuePostback = false;
/** /**
* Returns an input field. * Returns an input field.
* *
@ -39,12 +45,35 @@ class PasswordField extends TextField
parent::__construct($name, $title, $value); parent::__construct($name, $title, $value);
} }
/**
* @param bool $bool
* @return $this
*/
public function setAllowValuePostback($bool)
{
$this->allowValuePostback = (bool) $bool;
return $this;
}
/**
* @return bool
*/
public function getAllowValuePostback()
{
return $this->allowValuePostback;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getAttributes() public function getAttributes()
{ {
$attributes = array(); $attributes = [];
if (!$this->getAllowValuePostback()) {
$attributes['value'] = null;
}
$autocomplete = $this->config()->get('autocomplete'); $autocomplete = $this->config()->get('autocomplete');

View File

@ -333,7 +333,7 @@ class MarkedSet
$parentNode->setField('markingClasses', $this->markingClasses($data['node'])); $parentNode->setField('markingClasses', $this->markingClasses($data['node']));
// Evaluate custom context // Evaluate custom context
if (is_callable($context)) { if (!is_string($context) && is_callable($context)) {
$context = call_user_func($context, $data['node']); $context = call_user_func($context, $data['node']);
} }
if ($context) { if ($context) {

View File

@ -33,6 +33,7 @@ use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\Map; use SilverStripe\ORM\Map;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
@ -60,27 +61,26 @@ use SilverStripe\ORM\ValidationResult;
*/ */
class Member extends DataObject class Member extends DataObject
{ {
private static $db = array( private static $db = array(
'FirstName' => 'Varchar', 'FirstName' => 'Varchar',
'Surname' => 'Varchar', 'Surname' => 'Varchar',
'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character) 'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication 'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication
'TempIDExpired' => 'Datetime', // Expiry of temp login 'TempIDExpired' => 'Datetime', // Expiry of temp login
'Password' => 'Varchar(160)', 'Password' => 'Varchar(160)',
'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset 'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
'AutoLoginExpired' => 'Datetime', 'AutoLoginExpired' => 'Datetime',
// This is an arbitrary code pointing to a PasswordEncryptor instance, // This is an arbitrary code pointing to a PasswordEncryptor instance,
// not an actual encryption algorithm. // not an actual encryption algorithm.
// Warning: Never change this field after its the first password hashing without // Warning: Never change this field after its the first password hashing without
// providing a new cleartext password as well. // providing a new cleartext password as well.
'PasswordEncryption' => "Varchar(50)", 'PasswordEncryption' => "Varchar(50)",
'Salt' => 'Varchar(50)', 'Salt' => 'Varchar(50)',
'PasswordExpiry' => 'Date', 'PasswordExpiry' => 'Date',
'LockedOutUntil' => 'Datetime', 'LockedOutUntil' => 'Datetime',
'Locale' => 'Varchar(6)', 'Locale' => 'Varchar(6)',
// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
'FailedLoginCount' => 'Int', 'FailedLoginCount' => 'Int',
); );
private static $belongs_many_many = array( private static $belongs_many_many = array(
@ -88,7 +88,7 @@ class Member extends DataObject
); );
private static $has_many = array( private static $has_many = array(
'LoggedPasswords' => MemberPassword::class, 'LoggedPasswords' => MemberPassword::class,
'RememberLoginHashes' => RememberLoginHash::class, 'RememberLoginHashes' => RememberLoginHash::class,
); );
@ -312,7 +312,7 @@ class Member extends DataObject
break; break;
} }
} }
return $result; return $result;
} }
/** /**
@ -521,10 +521,9 @@ class Member extends DataObject
'This method is deprecated and now does not add value. Please use Security::getCurrentUser()' 'This method is deprecated and now does not add value. Please use Security::getCurrentUser()'
); );
if ($member = Security::getCurrentUser()) { $member = Security::getCurrentUser();
if ($member && $member->exists()) { if ($member && $member->exists()) {
return true; return true;
}
} }
return false; return false;
@ -655,7 +654,7 @@ class Member extends DataObject
{ {
/** @var Member $member */ /** @var Member $member */
$member = static::get()->filter([ $member = static::get()->filter([
'AutoLoginHash' => $hash, 'AutoLoginHash' => $hash,
'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(), 'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(),
])->first(); ])->first();
@ -799,6 +798,7 @@ class Member extends DataObject
* @param Member|null|int $member Member or member ID to log in as. * @param Member|null|int $member Member or member ID to log in as.
* Set to null or 0 to act as a logged out user. * Set to null or 0 to act as a logged out user.
* @param callable $callback * @param callable $callback
* @return mixed Result of $callback
*/ */
public static function actAs($member, $callback) public static function actAs($member, $callback)
{ {
@ -831,11 +831,11 @@ class Member extends DataObject
'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore' 'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore'
); );
if ($member = Security::getCurrentUser()) { $member = Security::getCurrentUser();
if ($member) {
return $member->ID; return $member->ID;
} else {
return 0;
} }
return 0;
} }
/** /**
@ -892,8 +892,8 @@ class Member extends DataObject
'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))', 'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
'Values in brackets show "fieldname = value", usually denoting an existing email address', 'Values in brackets show "fieldname = value", usually denoting an existing email address',
array( array(
'id' => $existingRecord->ID, 'id' => $existingRecord->ID,
'name' => $identifierField, 'name' => $identifierField,
'value' => $this->$identifierField 'value' => $this->$identifierField
) )
)); ));
@ -912,7 +912,11 @@ class Member extends DataObject
->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail') ->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail')
->setData($this) ->setData($this)
->setTo($this->Email) ->setTo($this->Email)
->setSubject(_t(__CLASS__ . '.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject')) ->setSubject(_t(
__CLASS__ . '.SUBJECTPASSWORDCHANGED',
"Your password has been changed",
'Email subject'
))
->send(); ->send();
} }
@ -974,17 +978,26 @@ class Member extends DataObject
* @return bool True if the change can be accepted * @return bool True if the change can be accepted
*/ */
public function onChangeGroups($ids) public function onChangeGroups($ids)
{
// Ensure none of these match disallowed list
$disallowedGroupIDs = $this->disallowedGroups();
return count(array_intersect($ids, $disallowedGroupIDs)) == 0;
}
/**
* List of group IDs this user is disallowed from
*
* @return int[] List of group IDs
*/
protected function disallowedGroups()
{ {
// unless the current user is an admin already OR the logged in user is an admin // unless the current user is an admin already OR the logged in user is an admin
if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) { if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
return true; return [];
} }
// If there are no admin groups in this set then it's ok // Non-admins may not belong to admin groups
$adminGroups = Permission::get_groups_by_permission('ADMIN'); return Permission::get_groups_by_permission('ADMIN')->column('ID');
$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
return count(array_intersect($ids, $adminGroupIDs)) == 0;
} }
@ -1160,7 +1173,7 @@ class Member extends DataObject
if (!$format) { if (!$format) {
$format = [ $format = [
'columns' => ['Surname', 'FirstName'], 'columns' => ['Surname', 'FirstName'],
'sep' => ' ', 'sep' => ' ',
]; ];
} }
@ -1288,7 +1301,7 @@ class Member extends DataObject
} }
/** /**
* @return ManyManyList * @return ManyManyList|UnsavedRelationList
*/ */
public function DirectGroups() public function DirectGroups()
{ {
@ -1469,8 +1482,14 @@ class Member extends DataObject
$fields->removeByName('RememberLoginHashes'); $fields->removeByName('RememberLoginHashes');
if (Permission::check('EDIT_PERMISSIONS')) { if (Permission::check('EDIT_PERMISSIONS')) {
// Filter allowed groups
$groups = Group::get();
$disallowedGroupIDs = $this->disallowedGroups();
if ($disallowedGroupIDs) {
$groups = $groups->exclude('ID', $disallowedGroupIDs);
}
$groupsMap = array(); $groupsMap = array();
foreach (Group::get() as $group) { foreach ($groups as $group) {
// Listboxfield values are escaped, use ASCII char instead of &raquo; // Listboxfield values are escaped, use ASCII char instead of &raquo;
$groupsMap[$group->ID] = $group->getBreadcrumbs(' > '); $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
} }
@ -1525,7 +1544,11 @@ class Member extends DataObject
/** @skipUpgrade */ /** @skipUpgrade */
$labels['Email'] = _t(__CLASS__ . '.EMAIL', 'Email'); $labels['Email'] = _t(__CLASS__ . '.EMAIL', 'Email');
$labels['Password'] = _t(__CLASS__ . '.db_Password', 'Password'); $labels['Password'] = _t(__CLASS__ . '.db_Password', 'Password');
$labels['PasswordExpiry'] = _t(__CLASS__ . '.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date'); $labels['PasswordExpiry'] = _t(
__CLASS__ . '.db_PasswordExpiry',
'Password Expiry Date',
'Password expiry date'
);
$labels['LockedOutUntil'] = _t(__CLASS__ . '.db_LockedOutUntil', 'Locked out until', 'Security related date'); $labels['LockedOutUntil'] = _t(__CLASS__ . '.db_LockedOutUntil', 'Locked out until', 'Security related date');
$labels['Locale'] = _t(__CLASS__ . '.db_Locale', 'Interface Locale'); $labels['Locale'] = _t(__CLASS__ . '.db_Locale', 'Interface Locale');
if ($includerelations) { if ($includerelations) {
@ -1680,8 +1703,8 @@ class Member extends DataObject
* *
* This method will encrypt the password prior to writing. * This method will encrypt the password prior to writing.
* *
* @param string $password Cleartext password * @param string $password Cleartext password
* @param bool $write Whether to write the member afterwards * @param bool $write Whether to write the member afterwards
* @return ValidationResult * @return ValidationResult
*/ */
public function changePassword($password, $write = true) public function changePassword($password, $write = true)

View File

@ -91,6 +91,11 @@ class MemberAuthenticator implements Authenticator
// Validate against member if possible // Validate against member if possible
if ($member && !$asDefaultAdmin) { if ($member && !$asDefaultAdmin) {
$this->checkPassword($member, $data['Password'], $result); $this->checkPassword($member, $data['Password'], $result);
} elseif (!$asDefaultAdmin) {
// spoof a login attempt
$tempMember = Member::create();
$tempMember->{Member::config()->get('unique_identifier_field')} = $email;
$tempMember->validateCanLogin($result);
} }
// Emit failure to member and form (if available) // Emit failure to member and form (if available)
@ -164,7 +169,9 @@ class MemberAuthenticator implements Authenticator
*/ */
protected function recordLoginAttempt($data, HTTPRequest $request, $member, $success) protected function recordLoginAttempt($data, HTTPRequest $request, $member, $success)
{ {
if (!Security::config()->get('login_recording')) { if (!Security::config()->get('login_recording')
&& !Member::config()->get('lock_out_after_incorrect_logins')
) {
return; return;
} }

View File

@ -326,7 +326,7 @@ class SSViewer_DataPresenter extends SSViewer_Scope
$override = $overrides[$property]; $override = $overrides[$property];
// Late-evaluate this value // Late-evaluate this value
if (is_callable($override)) { if (!is_string($override) && is_callable($override)) {
$override = $override(); $override = $override();
// Late override may yet return null // Late override may yet return null

View File

@ -393,6 +393,10 @@ class DirectorTest extends SapphireTest
$this->assertFalse(Director::is_site_url("http://test.com?url=" . Director::absoluteBaseURL())); $this->assertFalse(Director::is_site_url("http://test.com?url=" . Director::absoluteBaseURL()));
$this->assertFalse(Director::is_site_url("http://test.com?url=" . urlencode(Director::absoluteBaseURL()))); $this->assertFalse(Director::is_site_url("http://test.com?url=" . urlencode(Director::absoluteBaseURL())));
$this->assertFalse(Director::is_site_url("//test.com?url=" . Director::absoluteBaseURL())); $this->assertFalse(Director::is_site_url("//test.com?url=" . Director::absoluteBaseURL()));
$this->assertFalse(Director::is_site_url('http://google.com\@test.com'));
$this->assertFalse(Director::is_site_url('http://google.com/@test.com'));
$this->assertFalse(Director::is_site_url('http://google.com:pass\@test.com'));
$this->assertFalse(Director::is_site_url('http://google.com:pass/@test.com'));
} }
/** /**

View File

@ -20,17 +20,17 @@ class ParameterConfirmationTokenTest extends SapphireTest
protected function setUp() protected function setUp()
{ {
parent::setUp(); parent::setUp();
$get = []; $_GET = [];
$get['parameterconfirmationtokentest_notoken'] = 'value'; $_GET['parameterconfirmationtokentest_notoken'] = 'value';
$get['parameterconfirmationtokentest_empty'] = ''; $_GET['parameterconfirmationtokentest_empty'] = '';
$get['parameterconfirmationtokentest_withtoken'] = '1'; $_GET['parameterconfirmationtokentest_withtoken'] = '1';
$get['parameterconfirmationtokentest_withtokentoken'] = 'dummy'; $_GET['parameterconfirmationtokentest_withtokentoken'] = 'dummy';
$get['parameterconfirmationtokentest_nulltoken'] = '1'; $_GET['parameterconfirmationtokentest_nulltoken'] = '1';
$get['parameterconfirmationtokentest_nulltokentoken'] = null; $_GET['parameterconfirmationtokentest_nulltokentoken'] = null;
$get['parameterconfirmationtokentest_emptytoken'] = '1'; $_GET['parameterconfirmationtokentest_emptytoken'] = '1';
$get['parameterconfirmationtokentest_emptytokentoken'] = ''; $_GET['parameterconfirmationtokentest_emptytokentoken'] = '';
$get['BackURL'] = 'page?parameterconfirmationtokentest_backtoken=1'; $_GET['BackURL'] = 'page?parameterconfirmationtokentest_backtoken=1';
$this->request = new HTTPRequest('GET', 'anotherpage', $get); $this->request = new HTTPRequest('GET', 'anotherpage', $_GET);
$this->request->setSession(new Session([])); $this->request->setSession(new Session([]));
} }
@ -129,6 +129,11 @@ class ParameterConfirmationTokenTest extends SapphireTest
$this->request $this->request
); );
$this->assertEquals('parameterconfirmationtokentest_backtoken', $token->getName()); $this->assertEquals('parameterconfirmationtokentest_backtoken', $token->getName());
// Test prepare_tokens() unsets $_GET vars
$this->assertArrayNotHasKey('parameterconfirmationtokentest_notoken', $_GET);
$this->assertArrayNotHasKey('parameterconfirmationtokentest_empty', $_GET);
$this->assertArrayNotHasKey('parameterconfirmationtokentest_noparam', $_GET);
} }
public function dataProviderURLs() public function dataProviderURLs()

View File

@ -9,8 +9,10 @@ use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\FormRequestHandler; use SilverStripe\Forms\FormRequestHandler;
use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\Tests\FormRequestHandlerTest\TestForm; use SilverStripe\Forms\Tests\FormRequestHandlerTest\TestForm;
use SilverStripe\Forms\Tests\FormRequestHandlerTest\TestFormRequestHandler; use SilverStripe\Forms\Tests\FormRequestHandlerTest\TestFormRequestHandler;
use SilverStripe\Forms\TextField;
/** /**
* @skipUpgrade * @skipUpgrade

View File

@ -3,6 +3,8 @@
namespace SilverStripe\Forms\Tests; namespace SilverStripe\Forms\Tests;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\Tests\FormTest\TestController; use SilverStripe\Forms\Tests\FormTest\TestController;
use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken; use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken;
use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck; use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck;
@ -10,6 +12,7 @@ use SilverStripe\Forms\Tests\FormTest\Player;
use SilverStripe\Forms\Tests\FormTest\Team; use SilverStripe\Forms\Tests\FormTest\Team;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\NullSecurityToken; use SilverStripe\Security\NullSecurityToken;
use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\Security\RandomGenerator; use SilverStripe\Security\RandomGenerator;
use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\CSSContentParser;
@ -59,6 +62,17 @@ class FormTest extends FunctionalTest
); );
} }
/**
* @return array
*/
public function boolDataProvider()
{
return [
[false],
[true],
];
}
public function testLoadDataFromRequest() public function testLoadDataFromRequest()
{ {
$form = new Form( $form = new Form(
@ -915,6 +929,46 @@ class FormTest extends FunctionalTest
$this->assertEmpty($formData['ExtraFieldCheckbox']); $this->assertEmpty($formData['ExtraFieldCheckbox']);
} }
/**
* @dataProvider boolDataProvider
* @param bool $allow
*/
public function testPasswordPostback($allow)
{
$form = $this->getStubForm();
$form->enableSecurityToken();
$form->Fields()->push(
PasswordField::create('Password')
->setAllowValuePostback($allow)
);
$form->Actions()->push(FormAction::create('doSubmit'));
$request = new HTTPRequest(
'POST',
'FormTest_Controller/Form',
[],
[
'key1' => 'foo',
'Password' => 'hidden',
SecurityToken::inst()->getName() => 'fail',
'action_doSubmit' => 1,
]
);
$form->getRequestHandler()->httpSubmission($request);
$parser = new CSSContentParser($form->forTemplate());
$passwords = $parser->getBySelector('input#Password');
$this->assertNotNull($passwords);
$this->assertCount(1, $passwords);
/* @var \SimpleXMLElement $password */
$password = $passwords[0];
$attrs = iterator_to_array($password->attributes());
if ($allow) {
$this->assertArrayHasKey('value', $attrs);
$this->assertEquals('hidden', $attrs['value']);
} else {
$this->assertArrayNotHasKey('value', $attrs);
}
}
protected function getStubForm() protected function getStubForm()
{ {
return new Form( return new Form(

View File

@ -0,0 +1,46 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\PasswordField;
class PasswordFieldTest extends SapphireTest
{
public function boolDataProvider()
{
return [
[false],
[true]
];
}
/**
* @dataProvider boolDataProvider
* @param bool $bool
*/
public function testAutocomplete($bool)
{
Config::modify()->set(PasswordField::class, 'autocomplete', $bool);
$field = new PasswordField('test');
$attrs = $field->getAttributes();
$this->assertArrayHasKey('autocomplete', $attrs);
$this->assertEquals($bool ? 'on' : 'off', $attrs['autocomplete']);
}
/**
* @dataProvider boolDataProvider
* @param bool $bool
*/
public function testValuePostback($bool)
{
$field = (new PasswordField('test', 'test', 'password'))
->setAllowValuePostback($bool);
$attrs = $field->getAttributes();
$this->assertArrayHasKey('value', $attrs);
$this->assertEquals($bool ? 'password' : '', $attrs['value']);
}
}

View File

@ -243,7 +243,6 @@ class MemberAuthenticatorTest extends SapphireTest
public function testNonExistantMemberGetsLoginAttemptRecorded() public function testNonExistantMemberGetsLoginAttemptRecorded()
{ {
Security::config()->set('login_recording', true);
Member::config() Member::config()
->set('lock_out_after_incorrect_logins', 1) ->set('lock_out_after_incorrect_logins', 1)
->set('lock_out_delay_mins', 10); ->set('lock_out_delay_mins', 10);
@ -272,7 +271,6 @@ class MemberAuthenticatorTest extends SapphireTest
public function testNonExistantMemberGetsLockedOut() public function testNonExistantMemberGetsLockedOut()
{ {
Security::config()->set('login_recording', true);
Member::config() Member::config()
->set('lock_out_after_incorrect_logins', 1) ->set('lock_out_after_incorrect_logins', 1)
->set('lock_out_delay_mins', 10); ->set('lock_out_delay_mins', 10);

View File

@ -7,6 +7,7 @@ use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\ListboxField;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
@ -686,6 +687,8 @@ class MemberTest extends FunctionalTest
$staffMember = $this->objFromFixture(Member::class, 'staffmember'); $staffMember = $this->objFromFixture(Member::class, 'staffmember');
/** @var Member $adminMember */ /** @var Member $adminMember */
$adminMember = $this->objFromFixture(Member::class, 'admin'); $adminMember = $this->objFromFixture(Member::class, 'admin');
// Construct admin and non-admin gruops
$newAdminGroup = new Group(array('Title' => 'newadmin')); $newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write(); $newAdminGroup->write();
Permission::grant($newAdminGroup->ID, 'ADMIN'); Permission::grant($newAdminGroup->ID, 'ADMIN');
@ -718,6 +721,37 @@ class MemberTest extends FunctionalTest
); );
} }
/**
* Ensure DirectGroups listbox disallows admin-promotion
*/
public function testAllowedGroupsListbox()
{
/** @var Group $adminGroup */
$adminGroup = $this->objFromFixture(Group::class, 'admingroup');
/** @var Member $staffMember */
$staffMember = $this->objFromFixture(Member::class, 'staffmember');
/** @var Member $adminMember */
$adminMember = $this->objFromFixture(Member::class, 'admin');
// Ensure you can see the DirectGroups box
$this->logInWithPermission('EDIT_PERMISSIONS');
// Non-admin member field contains non-admin groups
/** @var ListboxField $staffListbox */
$staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
$this->assertArrayNotHasKey($adminGroup->ID, $staffListbox->getSource());
// admin member field contains admin group
/** @var ListboxField $adminListbox */
$adminListbox = $adminMember->getCMSFields()->dataFieldByName('DirectGroups');
$this->assertArrayHasKey($adminGroup->ID, $adminListbox->getSource());
// If logged in as admin, staff listbox has admin group
$this->logInWithPermission('ADMIN');
$staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
$this->assertArrayHasKey($adminGroup->ID, $staffListbox->getSource());
}
/** /**
* Test Member_GroupSet::add * Test Member_GroupSet::add
*/ */
@ -1486,6 +1520,7 @@ class MemberTest extends FunctionalTest
public function testChangePasswordWithExtensionsThatModifyValidationResult() public function testChangePasswordWithExtensionsThatModifyValidationResult()
{ {
// Default behaviour // Default behaviour
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'admin'); $member = $this->objFromFixture(Member::class, 'admin');
$result = $member->changePassword('my-secret-new-password'); $result = $member->changePassword('my-secret-new-password');
$this->assertInstanceOf(ValidationResult::class, $result); $this->assertInstanceOf(ValidationResult::class, $result);

View File

@ -109,6 +109,16 @@ class SSViewerTest extends SapphireTest
$this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U", '', $result))); $this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U", '', $result)));
} }
/**
* Ensure global methods aren't executed
*/
public function testTemplateExecution()
{
$data = new ArrayData([ 'Var' => 'phpinfo' ]);
$result = $data->renderWith("SSViewerTestPartialTemplate");
$this->assertEquals('Test partial template: phpinfo', trim(preg_replace("/<!--.*-->/U", '', $result)));
}
public function testIncludeScopeInheritance() public function testIncludeScopeInheritance()
{ {
$data = $this->getScopeInheritanceTestData(); $data = $this->getScopeInheritanceTestData();