Merge branch '4.1' into 4

This commit is contained in:
Robbie Averill 2018-05-28 18:33:56 +12:00
commit ea16e28aa7
26 changed files with 443 additions and 93 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

@ -0,0 +1,35 @@
# 4.1.1
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)
### Features and Enhancements
* 2017-12-21 [4d60f01](https://github.com/silverstripe/silverstripe-installer/commit/4d60f01d2dd17febcf15c08ecdc07af7380694d0) add test for a `--no-dev` build (Christopher Joe)
### 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-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

@ -5,7 +5,7 @@
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]--> <!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
<head> <head>
<title>SilverStripe CMS / Framework Installation</title> <title>SilverStripe CMS / Framework Installation</title>
<base href="<?php echo htmlentities($base); ?>/" /> <base href="<?php echo htmlentities($base); ?>" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8"> <meta http-equiv="Content-type" content="text/html; charset=utf-8">
<script type="application/javascript" src="//code.jquery.com/jquery-1.7.2.min.js"></script> <script type="application/javascript" src="//code.jquery.com/jquery-1.7.2.min.js"></script>
<script type="application/javascript" src="<?=$clientPath; ?>/js/install.js"></script> <script type="application/javascript" src="<?=$clientPath; ?>/js/install.js"></script>

View File

@ -105,7 +105,7 @@ if ($installFromCli && ($req->hasErrors() || $dbReq->hasErrors())) {
} }
// Path to client resources (copied through silverstripe/vendor-plugin) // Path to client resources (copied through silverstripe/vendor-plugin)
$base = BASE_URL; $base = rtrim(BASE_URL, '/') . '/';
$clientPath = PUBLIC_DIR $clientPath = PUBLIC_DIR
? 'resources/vendor/silverstripe/framework/src/Dev/Install/client' ? 'resources/vendor/silverstripe/framework/src/Dev/Install/client'
: 'resources/silverstripe/framework/src/Dev/Install/client'; : 'resources/silverstripe/framework/src/Dev/Install/client';

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

@ -39,39 +39,39 @@ class PasswordValidator
/** /**
* @config * @config
* @var integer * @var int
*/ */
private static $min_length = null; private static $min_length = null;
/** /**
* @config * @config
* @var integer * @var int
*/ */
private static $min_test_score = null; private static $min_test_score = null;
/** /**
* @config * @config
* @var integer * @var int
*/ */
private static $historic_count = null; private static $historic_count = null;
/** /**
* @var integer * @var int
*/ */
protected $minLength = null; protected $minLength = null;
/** /**
* @var integer * @var int
*/ */
protected $minScore = null; protected $minScore = null;
/** /**
* @var array * @var string[]
*/ */
protected $testNames = null; protected $testNames = null;
/** /**
* @var integer * @var int
*/ */
protected $historicalPasswordCount = null; protected $historicalPasswordCount = null;
@ -95,18 +95,20 @@ class PasswordValidator
* Eg: $this->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation")) * Eg: $this->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"))
* *
* @param int $minScore The minimum number of character tests that must pass * @param int $minScore The minimum number of character tests that must pass
* @param array $testNames The names of the tests to perform * @param string[] $testNames The names of the tests to perform
* @return $this * @return $this
*/ */
public function characterStrength($minScore, $testNames = null) public function characterStrength($minScore, $testNames = null)
{ {
Deprecation::notice( Deprecation::notice(
'5.0', '5.0',
'Use ->setMinTestScore($score) and ->setTextNames($names) instead.' 'Use ->setMinTestScore($score) and ->setTextNames($names) instead.'
); );
return $this->setMinTestScore($minScore) $this->setMinTestScore($minScore);
->setTestNames($testNames); if ($testNames) {
$this->setTestNames($testNames);
}
return $this;
} }
/** /**
@ -123,7 +125,7 @@ class PasswordValidator
} }
/** /**
* @return integer * @return int
*/ */
public function getMinLength() public function getMinLength()
{ {
@ -134,7 +136,7 @@ class PasswordValidator
} }
/** /**
* @param $minLength * @param int $minLength
* @return $this * @return $this
*/ */
public function setMinLength($minLength) public function setMinLength($minLength)
@ -155,7 +157,7 @@ class PasswordValidator
} }
/** /**
* @param $minScore * @param int $minScore
* @return $this * @return $this
*/ */
public function setMinTestScore($minScore) public function setMinTestScore($minScore)
@ -165,7 +167,9 @@ class PasswordValidator
} }
/** /**
* @return array * Gets the list of tests to use for this validator
*
* @return string[]
*/ */
public function getTestNames() public function getTestNames()
{ {
@ -176,7 +180,9 @@ class PasswordValidator
} }
/** /**
* @param $testNames * Set list of tests to use for this validator
*
* @param string[] $testNames
* @return $this * @return $this
*/ */
public function setTestNames($testNames) public function setTestNames($testNames)
@ -186,7 +192,7 @@ class PasswordValidator
} }
/** /**
* @return integer * @return int
*/ */
public function getHistoricCount() public function getHistoricCount()
{ {
@ -197,7 +203,7 @@ class PasswordValidator
} }
/** /**
* @param $count * @param int $count
* @return $this * @return $this
*/ */
public function setHistoricCount($count) public function setHistoricCount($count)
@ -207,6 +213,8 @@ class PasswordValidator
} }
/** /**
* Gets all possible tests
*
* @return array * @return array
*/ */
public function getTests() public function getTests()
@ -226,7 +234,7 @@ class PasswordValidator
$minLength = $this->getMinLength(); $minLength = $this->getMinLength();
if ($minLength && strlen($password) < $minLength) { if ($minLength && strlen($password) < $minLength) {
$error = _t( $error = _t(
'SilverStripe\\Security\\PasswordValidator.TOOSHORT', __CLASS__ . '.TOOSHORT',
'Password is too short, it must be {minimum} or more characters long', 'Password is too short, it must be {minimum} or more characters long',
['minimum' => $this->minLength] ['minimum' => $this->minLength]
); );
@ -245,16 +253,16 @@ class PasswordValidator
continue; continue;
} }
$missedTests[] = _t( $missedTests[] = _t(
'SilverStripe\\Security\\PasswordValidator.STRENGTHTEST' . strtoupper($name), __CLASS__ . '.STRENGTHTEST' . strtoupper($name),
$name, $name,
'The user needs to add this to their password for more complexity' 'The user needs to add this to their password for more complexity'
); );
} }
$score = count($this->testNames) - count($missedTests); $score = count($testNames) - count($missedTests);
if ($score < $minTestScore) { if ($missedTests && $score < $minTestScore) {
$error = _t( $error = _t(
'SilverStripe\\Security\\PasswordValidator.LOWCHARSTRENGTH', __CLASS__ . '.LOWCHARSTRENGTH',
'Please increase password strength by adding some of the following characters: {chars}', 'Please increase password strength by adding some of the following characters: {chars}',
['chars' => implode(', ', $missedTests)] ['chars' => implode(', ', $missedTests)]
); );
@ -271,8 +279,8 @@ class PasswordValidator
/** @var MemberPassword $previousPassword */ /** @var MemberPassword $previousPassword */
foreach ($previousPasswords as $previousPassword) { foreach ($previousPasswords as $previousPassword) {
if ($previousPassword->checkPassword($password)) { if ($previousPassword->checkPassword($password)) {
$error = _t( $error = _t(
'SilverStripe\\Security\\PasswordValidator.PREVPASSWORD', __CLASS__ . '.PREVPASSWORD',
'You\'ve already used that password in the past, please choose a new password' 'You\'ve already used that password in the past, please choose a new password'
); );
$valid->addError($error, 'bad', 'PREVIOUS_PASSWORD'); $valid->addError($error, 'bad', 'PREVIOUS_PASSWORD');

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

@ -8,8 +8,9 @@ class TestPasswordValidator extends PasswordValidator
{ {
public function __construct() public function __construct()
{ {
$this->minLength(7); $this->setMinLength(7);
$this->checkHistoricalPasswords(6); $this->setHistoricCount(6);
$this->characterStrength(3, array('lowercase', 'uppercase', 'digits', 'punctuation')); $this->setMinTestScore(3);
$this->setTestNames(['lowercase', 'uppercase', 'digits', 'punctuation']);
} }
} }

View File

@ -28,25 +28,42 @@ class PasswordValidatorTest extends SapphireTest
{ {
$v = new PasswordValidator(); $v = new PasswordValidator();
$v->minLength(4); $v->setMinLength(4);
$r = $v->validate('123', new Member()); $r = $v->validate('123', new Member());
$this->assertFalse($r->isValid(), 'Password too short'); $this->assertFalse($r->isValid(), 'Password too short');
$v->minLength(4); $v->setMinLength(4);
$r = $v->validate('1234', new Member()); $r = $v->validate('1234', new Member());
$this->assertTrue($r->isValid(), 'Password long enough'); $this->assertTrue($r->isValid(), 'Password long enough');
} }
public function testValidateMinScore() public function testValidateMinScore()
{ {
// Set both score and set of tests
$v = new PasswordValidator(); $v = new PasswordValidator();
$v->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation")); $v->setMinTestScore(3);
$v->setTestNames(["lowercase", "uppercase", "digits", "punctuation"]);
$r = $v->validate('aA', new Member()); $r = $v->validate('aA', new Member());
$this->assertFalse($r->isValid(), 'Passing too few tests'); $this->assertFalse($r->isValid(), 'Passing too few tests');
$r = $v->validate('aA1', new Member()); $r = $v->validate('aA1', new Member());
$this->assertTrue($r->isValid(), 'Passing enough tests'); $this->assertTrue($r->isValid(), 'Passing enough tests');
// Ensure min score without tests works (uses default tests)
$v = new PasswordValidator();
$v->setMinTestScore(3);
$r = $v->validate('aA', new Member());
$this->assertFalse($r->isValid(), 'Passing too few tests');
$r = $v->validate('aA1', new Member());
$this->assertTrue($r->isValid(), 'Passing enough tests');
// Ensure that min score is only triggered if there are any failing tests at all
$v->setMinTestScore(1000);
$r = $v->validate('aA1!', new Member());
$this->assertTrue($r->isValid(), 'Passing enough tests');
} }
/** /**
@ -55,7 +72,7 @@ class PasswordValidatorTest extends SapphireTest
public function testHistoricalPasswordCount() public function testHistoricalPasswordCount()
{ {
$validator = new PasswordValidator; $validator = new PasswordValidator;
$validator->checkHistoricalPasswords(3); $validator->setHistoricCount(3);
Member::set_password_validator($validator); Member::set_password_validator($validator);
$member = new Member; $member = new Member;

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();