Merge remote-tracking branch 'security/3.5.4' into 3.6.0

This commit is contained in:
Daniel Hensby 2017-05-29 01:29:05 +01:00
commit cda7e8dc39
No known key found for this signature in database
GPG Key ID: B00D1E9767F0B06E
22 changed files with 279 additions and 116 deletions

View File

@ -726,16 +726,17 @@ abstract class Object {
* @return mixed
*/
public function __call($method, $arguments) {
$class = get_class($this);
// If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
// call, then we should rebuild it.
if(empty(self::$extra_methods[get_class($this)])) {
if(empty(self::$extra_methods[$class])) {
$this->defineMethods();
}
$method = strtolower($method);
if(isset(self::$extra_methods[$this->class][$method])) {
$config = self::$extra_methods[$this->class][$method];
if(isset(self::$extra_methods[$class][$method])) {
$config = self::$extra_methods[$class][$method];
switch(true) {
case isset($config['property']) :
@ -752,11 +753,11 @@ abstract class Object {
if($this->destroyed) {
throw new Exception (
"Object->__call(): attempt to call $method on a destroyed $this->class object"
"Object->__call(): attempt to call $method on a destroyed $class object"
);
} else {
throw new Exception (
"Object->__call(): $this->class cannot pass control to $config[property]($config[index])."
"Object->__call(): $class cannot pass control to $config[property]($config[index])."
. ' Perhaps this object was mistakenly destroyed?'
);
}
@ -770,13 +771,12 @@ abstract class Object {
default :
throw new Exception (
"Object->__call(): extra method $method is invalid on $this->class:"
"Object->__call(): extra method $method is invalid on $class:"
. var_export($config, true)
);
}
} else {
// Please do not change the exception code number below.
$class = get_class($this);
throw new Exception("Object->__call(): the method '$method' does not exist on '$class', or the method is not public.", 2175);
}
}
@ -793,7 +793,7 @@ abstract class Object {
* @return bool
*/
public function hasMethod($method) {
return method_exists($this, $method) || isset(self::$extra_methods[$this->class][strtolower($method)]);
return method_exists($this, $method) || isset(self::$extra_methods[get_class($this)][strtolower($method)]);
}
/**
@ -803,14 +803,15 @@ abstract class Object {
* @return array
*/
public function allMethodNames($custom = false) {
if(!isset(self::$built_in_methods[$this->class])) {
self::$built_in_methods[$this->class] = array_map('strtolower', get_class_methods($this));
$class = get_class($this);
if(!isset(self::$built_in_methods[$class])) {
self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
}
if($custom && isset(self::$extra_methods[$this->class])) {
return array_merge(self::$built_in_methods[$this->class], array_keys(self::$extra_methods[$this->class]));
if($custom && isset(self::$extra_methods[$class])) {
return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
} else {
return self::$built_in_methods[$this->class];
return self::$built_in_methods[$class];
}
}
@ -826,11 +827,12 @@ abstract class Object {
$this->addMethodsFrom('extension_instances', $key);
}
if(isset($_REQUEST['debugmethods']) && isset(self::$built_in_methods[$this->class])) {
$class = get_class($this);
if(isset($_REQUEST['debugmethods']) && isset(self::$built_in_methods[$class])) {
Debug::require_developer_login();
echo '<h2>Methods defined on ' . $this->class . '</h2><ul>';
foreach(self::$built_in_methods[$this->class] as $method) {
echo "<h2>Methods defined on $class</h2><ul>";
foreach(self::$built_in_methods[$class] as $method) {
echo "<li>$method</li>";
}
echo '</ul>';
@ -863,11 +865,12 @@ abstract class Object {
* @param string|int $index an index to use if the property is an array
*/
protected function addMethodsFrom($property, $index = null) {
$class = get_class($this);
$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
if(!$extension) {
throw new InvalidArgumentException (
"Object->addMethodsFrom(): could not add methods from {$this->class}->{$property}[$index]"
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
);
}
@ -881,11 +884,11 @@ abstract class Object {
$newMethods = array_fill_keys($methods, $methodInfo);
if(isset(self::$extra_methods[$this->class])) {
self::$extra_methods[$this->class] =
array_merge(self::$extra_methods[$this->class], $newMethods);
if(isset(self::$extra_methods[$class])) {
self::$extra_methods[$class] =
array_merge(self::$extra_methods[$class], $newMethods);
} else {
self::$extra_methods[$this->class] = $newMethods;
self::$extra_methods[$class] = $newMethods;
}
}
}
@ -929,7 +932,7 @@ abstract class Object {
* @param string $wrap the method name to wrap to
*/
protected function addWrapperMethod($method, $wrap) {
self::$extra_methods[$this->class][strtolower($method)] = array (
self::$extra_methods[get_class($this)][strtolower($method)] = array (
'wrap' => $wrap,
'method' => $method
);
@ -944,7 +947,7 @@ abstract class Object {
* function
*/
protected function createMethod($method, $code) {
self::$extra_methods[$this->class][strtolower($method)] = array (
self::$extra_methods[get_class($this)][strtolower($method)] = array (
'function' => create_function('$obj, $args', $code)
);
}

View File

@ -270,7 +270,7 @@ class PaginatedList extends SS_ListDecorator {
* </code>
*
* @param int $context The number of pages to display around the current
* page. The number should be event, as half the number of each pages
* page. The number should be even, as half the number of each pages
* are displayed on either side of the current one.
* @return SS_List
*/

View File

@ -0,0 +1,18 @@
# 3.4.6-rc1
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2017-05-24 [41270fc](https://github.com/silverstripe/silverstripe-cms/commit/41270fcf9980c4be2529d2750c717675548eb617) Only allow HTTP(S) links for external redirector pages (Daniel Hensby) - See [ss-2017-003](http://www.silverstripe.org/download/security-releases/ss-2017-003)
* 2017-05-09 [447ce0f](https://github.com/silverstripe/silverstripe-framework/commit/447ce0f84f880c2bc969a89e4be528c53caeabe0) Lock out users who dont exist in the DB (Daniel Hensby) - See [ss-2017-002](http://www.silverstripe.org/download/security-releases/ss-2017-002)
* 2017-05-09 [61cf72c](https://github.com/silverstripe/silverstripe-cms/commit/61cf72c08dafddef416d73f943ccd45e70c5d43d) Unescaped fields in CMSPageHistroyController::compare() (Daniel Hensby) - See [ss-2017-004](http://www.silverstripe.org/download/security-releases/ss-2017-004)
### Bugfixes
* 2017-05-03 [2d138b0](https://github.com/silverstripe/silverstripe-framework/commit/2d138b0ef06bd93958cc0678a0afa95560648fb9) class name reference consistency (Gregory Smirnov)
* 2017-04-24 [1d36f35](https://github.com/silverstripe/silverstripe-framework/commit/1d36f354e8349616c7b39fcade859fbcf0f9c362) Create Image_Cached with Injector. (Gregory Smirnov)
* 2017-02-15 [3072591](https://github.com/silverstripe/silverstripe-framework/commit/30725916dbb0ffc66b77f26c069a86581636ae55) Array to string conversion message after CSV export (#6622) (Juan van den Anker)
* 2017-02-14 [7122e1f](https://github.com/silverstripe/silverstripe-framework/commit/7122e1fde79bdb9aad3c8714a6ce02b7ecedd735) Comments ignored by classmanifest (#6619) (Daniel Hensby)

View File

@ -0,0 +1,9 @@
# 3.4.6-rc2
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Bugfixes
* 2017-05-28 [16a74bc](https://github.com/silverstripe/silverstripe-framework/commit/16a74bc8a9fdee7cfb4f6f24493c271f90a76341) DataDifferencer needs to expliclty cast HTMLText values (Daniel Hensby)

View File

@ -0,0 +1,28 @@
# 3.5.4-rc1
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2017-05-24 [41270fc](https://github.com/silverstripe/silverstripe-cms/commit/41270fcf9980c4be2529d2750c717675548eb617) Only allow HTTP(S) links for external redirector pages (Daniel Hensby) - See [ss-2017-003](http://www.silverstripe.org/download/security-releases/ss-2017-003)
* 2017-05-09 [447ce0f](https://github.com/silverstripe/silverstripe-framework/commit/447ce0f84f880c2bc969a89e4be528c53caeabe0) Lock out users who dont exist in the DB (Daniel Hensby) - See [ss-2017-002](http://www.silverstripe.org/download/security-releases/ss-2017-002)
* 2017-05-09 [61cf72c](https://github.com/silverstripe/silverstripe-cms/commit/61cf72c08dafddef416d73f943ccd45e70c5d43d) Unescaped fields in CMSPageHistroyController::compare() (Daniel Hensby) - See [ss-2017-004](http://www.silverstripe.org/download/security-releases/ss-2017-004)
### Bugfixes
* 2017-05-28 [16a74bc](https://github.com/silverstripe/silverstripe-framework/commit/16a74bc8a9fdee7cfb4f6f24493c271f90a76341) DataDifferencer needs to expliclty cast HTMLText values (Daniel Hensby)
* 2017-05-08 [1454072](https://github.com/silverstripe/silverstripe-cms/commit/14540729caa30dd2e782e4fd52afe518dc156ed8) Use framework 3.5 to test cms 3.5 (Sam Minnee)
* 2017-05-03 [2d138b0](https://github.com/silverstripe/silverstripe-framework/commit/2d138b0ef06bd93958cc0678a0afa95560648fb9) class name reference consistency (Gregory Smirnov)
* 2017-05-02 [2187c16](https://github.com/silverstripe/silverstripe-framework/commit/2187c160b936620621fe746a1ffe36af568b21ff) ing pagination api doc typo (3Dgoo)
* 2017-04-28 [a511e35](https://github.com/silverstripe/silverstripe-framework/commit/a511e3511cace405dab7589a3406a0858cb6edf2) #6855: Mangled JS in Requirements, escaping replacement values prior to passing to preg_replace(). (Patrick Nelson)
* 2017-04-24 [1d36f35](https://github.com/silverstripe/silverstripe-framework/commit/1d36f354e8349616c7b39fcade859fbcf0f9c362) Create Image_Cached with Injector. (Gregory Smirnov)
* 2017-04-07 [55eb7eb](https://github.com/silverstripe/silverstripe-framework/commit/55eb7ebdcc9ba767f978dff510614bbd2e0c309d) Do not insert requirements more than once in includeInHTML (Robbie Averill)
* 2017-04-05 [a7920b1](https://github.com/silverstripe/silverstripe-framework/commit/a7920b1f9866b6eb5f4bad9de84eef84b88673ad) regression from #6668 - ModelAdmin form widths (Loz Calver)
* 2017-04-05 [197bc53](https://github.com/silverstripe/silverstripe-framework/commit/197bc53c4963898d2c10621ca6d6031fdb14fe85) Add transparency percent argument to Image::generatePad to ensure transparency works from ::Pad (Robbie Averill)
* 2017-02-15 [3072591](https://github.com/silverstripe/silverstripe-framework/commit/30725916dbb0ffc66b77f26c069a86581636ae55) Array to string conversion message after CSV export (#6622) (Juan van den Anker)
* 2017-02-14 [7122e1f](https://github.com/silverstripe/silverstripe-framework/commit/7122e1fde79bdb9aad3c8714a6ce02b7ecedd735) Comments ignored by classmanifest (#6619) (Daniel Hensby)
* 2017-02-09 [6e2797f](https://github.com/silverstripe/silverstripe-framework/commit/6e2797ffc0e9632b60acc5a66e52aeb44f0e2b78) es for using dblib PDO driver. (Andrew O'Neil)
* 2017-02-08 [c25c443](https://github.com/silverstripe/silverstripe-framework/commit/c25c443d95fc305fb3545b1393b7da85923dcf8b) Fix minor mysql 5.7 warning in SQLQueryTest (#6608) (Damian Mooyman)
* 2017-01-18 [72b6fb4](https://github.com/silverstripe/silverstripe-framework/commit/72b6fb49b698bc3a51c8f6b32d2bf08213729493) bug: In addOrderBy method, _SortColumn will only keep the last one if there are more than 1 multi-word columns (Shawn)

View File

@ -102,6 +102,29 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
return $dataListClone;
}
/**
* @param string $class
* @param string $column
* @return string
*/
protected function columnToFilterField($class, $column)
{
if (strpos($column, '.') === false) {
return $column;
}
/** @var DataObject $model */
$model = singleton($class);
$columnParts = explode('.', $column);
if ($model->getRelationClass($columnParts[0])) {
return $column;
}
return $columnParts[0];
}
/**
* @param GridField $gridField
* @return array
*/
public function getHTMLFragments($gridField) {
if(!$this->checkDataType($gridField->getList())) return;
@ -110,18 +133,21 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
$columns = $gridField->getColumns();
$filterArguments = $gridField->State->GridFieldFilterHeader->Columns->toArray();
$currentColumn = 0;
foreach($columns as $columnField) {
$currentColumn++;
++$currentColumn;
$metadata = $gridField->getColumnMetadata($columnField);
$title = $metadata['title'];
$fields = new FieldGroup();
if($title && $gridField->getList()->canFilterBy($columnField)) {
$filterField = $this->columnToFilterField($gridField->getModelClass(), $columnField);
if($title && $gridField->getList()->canFilterBy($filterField)) {
$value = '';
if(isset($filterArguments[$columnField])) {
$value = $filterArguments[$columnField];
if(isset($filterArguments[$filterField])) {
$value = $filterArguments[$filterField];
}
$field = new TextField('filter[' . $gridField->getName() . '][' . $columnField . ']', '', $value);
$field = new TextField('filter[' . $gridField->getName() . '][' . $filterField . ']', '', $value);
$field->addExtraClass('ss-gridfield-sort');
$field->addExtraClass('no-change-track');
@ -133,7 +159,7 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
GridField_FormAction::create($gridField, 'reset', false, 'reset', null)
->addExtraClass('ss-gridfield-button-reset')
->setAttribute('title', _t('GridField.ResetFilter', "Reset"))
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField)
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $filterField)
);
}

View File

@ -103,7 +103,7 @@ class DataDifferencer extends ViewableData {
// Show changes between the two, if any exist
if($fromValue != $toValue) {
$diffed->setField($field, Diff::compareHTML($fromValue, $toValue));
$diffed->setField($field, DBField::create_field('HTMLText', Diff::compareHTML($fromValue, $toValue)));
}
}

View File

@ -339,10 +339,11 @@ class Image extends File implements Flushable {
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @param string $backgroundColor The background colour to use on padded sides (default white)
* @param integer $transparencyPercent Level of transparency
* @return Image|null
*/
public function Pad($width, $height, $backgroundColor='FFFFFF', $transparencyPercent = 0) {
public function Pad($width, $height, $backgroundColor = 'FFFFFF', $transparencyPercent = 0) {
return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample')
? $this
: $this->getFormattedImage('Pad', $width, $height, $backgroundColor, $transparencyPercent);
@ -354,12 +355,16 @@ class Image extends File implements Flushable {
* @param Image_Backend $backend
* @param integer $width The width to size to
* @param integer $height The height to size to
* @param string $backgroundColor The background colour to use on padded sides (default white)
* @param integer $transparencyPercent Level of transparency
* @return Image_Backend
* @deprecated 4.0 Generate methods are no longer applicable
*/
public function generatePad(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
public function generatePad(Image_Backend $backend, $width, $height, $backgroundColor = 'FFFFFF',
$transparencyPercent = 0
) {
Deprecation::notice('4.0', 'Generate methods are no longer applicable');
return $backend->paddedResize($width, $height, $backgroundColor);
return $backend->paddedResize($width, $height, $backgroundColor, $transparencyPercent);
}
/**
@ -730,7 +735,7 @@ class Image extends File implements Flushable {
call_user_func_array(array($this, "generateFormattedImage"), $args);
}
$cached = new Image_Cached($cacheFile, false, $this);
$cached = Injector::inst()->createWithArgs('Image_Cached', array($cacheFile, false, $this));
return $cached;
}
}

View File

@ -398,9 +398,44 @@ class Member extends DataObject implements TemplateGlobalProvider {
* Returns true if this user is locked out
*/
public function isLockedOut() {
$state = ($this->LockedOutUntil && SS_Datetime::now()->Format('U') < strtotime($this->LockedOutUntil));
$this->extend('updateIsLockedOut', $state);
return $state;
$state = true;
if ($this->LockedOutUntil && $this->dbObject('LockedOutUntil')->InFuture()) {
$state = true;
} elseif ($this->config()->lock_out_after_incorrect_logins <= 0) {
$state = false;
} else {
$attempts = LoginAttempt::get()->filter($filter = array(
'Email' => $this->{static::config()->unique_identifier_field},
))->sort('Created', 'DESC')->limit($this->config()->lock_out_after_incorrect_logins);
if ($attempts->count() < $this->config()->lock_out_after_incorrect_logins) {
$state = false;
} else {
$success = false;
foreach ($attempts as $attempt) {
if ($attempt->Status === 'Success') {
$success = true;
$state = false;
break;
}
}
if (!$success) {
$lockedOutUntil = $attempts->first()->dbObject('Created')->Format('U')
+ ($this->config()->lock_out_delay_mins * 60);
if (SS_Datetime::now()->Format('U') < $lockedOutUntil) {
$state = true;
} else {
$state = false;
}
}
}
}
$this->extend('updateIsLockedOut', $state);
return $state;
}
/**
@ -1666,7 +1701,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public function registerFailedLogin() {
if(self::config()->lock_out_after_incorrect_logins) {
// Keep a tally of the number of failed log-ins so that we can lock people out
$this->FailedLoginCount = $this->FailedLoginCount + 1;
++$this->FailedLoginCount;
if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
$lockoutMins = self::config()->lock_out_delay_mins;
@ -1685,6 +1720,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
if(self::config()->lock_out_after_incorrect_logins) {
// Forgive all past login failures
$this->FailedLoginCount = 0;
$this->LockedOutUntil = null;
$this->write();
}
$this->extend('onAfterRegisterSuccessfulLogin');

View File

@ -70,6 +70,14 @@ class MemberAuthenticator extends Authenticator {
if($member && !$asDefaultAdmin) {
$result = $member->checkPassword($data['Password']);
$success = $result->valid();
} elseif (!$asDefaultAdmin) {
// spoof a login attempt
$member = Member::create();
$member->Email = $email;
$member->{Member::config()->unique_identifier_field} = $data['Password'] . '-wrong';
$member->PasswordEncryption = 'none';
$result = $member->checkPassword($data['Password']);
$member = null;
} else {
$result = new ValidationResult(false, _t('Member.ERRORWRONGCRED'));
}
@ -94,7 +102,7 @@ class MemberAuthenticator extends Authenticator {
* @param bool $success
*/
protected static function record_login_attempt($data, $member, $success) {
if(!Security::config()->login_recording) return;
if(!Security::config()->login_recording && !Member::config()->lock_out_after_incorrect_logins) return;
// Check email is valid
$email = isset($data['Email']) ? $data['Email'] : null;

View File

@ -6,16 +6,9 @@ class CookieTest extends SapphireTest {
public function setUp() {
parent::setUp();
Injector::nest();
Injector::inst()->registerService(new CookieJar($_COOKIE), 'Cookie_Backend');
}
public function tearDown() {
//restore the cookie_backend
Injector::unnest();
parent::tearDown();
}
/**
* Check a new cookie inst will be loaded with the superglobal by default
*/

View File

@ -134,8 +134,6 @@ class UploadTest extends SapphireTest {
}
public function testGetAllowedMaxFileSize() {
Config::nest();
// Check the max file size uses the config values
$configMaxFileSizes = array(
'[image]' => '1k',
@ -176,15 +174,13 @@ class UploadTest extends SapphireTest {
$retrievedSize = $v->getAllowedMaxFileSize('txt');
$this->assertEquals(4096, $retrievedSize, 'Max file size check on instance values failed (instance extension set check)');
// Check a wildcard max file size against a file with an extension
$v = new UploadTest_Validator();
$v->setAllowedMaxFileSize(2000);
$retrievedSize = $v->getAllowedMaxFileSize('.jpg');
$this->assertEquals(2000, $retrievedSize, 'Max file size check on instance values failed (wildcard max file size)');
Config::unnest();
}
public function testAllowedSizeOnFileWithNoExtension() {

View File

@ -10,8 +10,6 @@ class FormFieldTest extends SapphireTest {
);
public function testDefaultClasses() {
Config::nest();
Config::inst()->update('FormField', 'default_classes', array(
'class1',
));
@ -50,8 +48,6 @@ class FormFieldTest extends SapphireTest {
//check default classes inherit
$this->assertContains('class3', $field->extraClass(), 'Class list does not contain inherited class');
$this->assertContains('textfield-class', $field->extraClass(), 'Class list does not contain expected class');
Config::unnest();
}
public function testAddExtraClass() {

View File

@ -549,8 +549,6 @@ class FormTest extends FunctionalTest {
}
public function testDefaultClasses() {
Config::nest();
Config::inst()->update('Form', 'default_classes', array(
'class1',
));
@ -579,8 +577,6 @@ class FormTest extends FunctionalTest {
$form->removeExtraClass('class3');
$this->assertNotContains('class3', $form->extraClass(), 'Class list contains unexpected class');
Config::unnest();
}
public function testAttributes() {

View File

@ -25,17 +25,10 @@ class HtmlEditorFieldToolbarTest extends SapphireTest {
public function setUp() {
parent::setUp();
Config::nest();
Config::inst()->update('HtmlEditorField_Toolbar', 'fileurl_scheme_whitelist', array('http'));
Config::inst()->update('HtmlEditorField_Toolbar', 'fileurl_domain_whitelist', array('example.com'));
}
public function tearDown() {
Config::unnest();
parent::tearDown();
}
public function testValidLocalReference() {
list($file, $url) = $this->getToolbar()->viewfile_getLocalFileByURL('folder/subfolder/example.pdf');
$this->assertEquals($this->objFromFixture('File', 'example_file'), $file);

View File

@ -0,0 +1,32 @@
<?php
class GridFieldFilterHeaderTest extends SapphireTest {
protected $extraDataObjects = array(
'GridFieldFilterHeaderTest_DataObject',
);
public function testColumnToFilterField() {
$class = 'GridFieldFilterHeaderTest_DataObject';
$header = new GridFieldFilterHeader();
$method = new ReflectionMethod($header, 'columnToFilterField');
$method->setAccessible(true);
$this->assertEquals('Title', $method->invoke($header, $class,'Title.ATT'));
$this->assertEquals('isTest', $method->invoke($header, $class, 'isTest.Nice'));
$this->assertEquals('Self.isTest.Nice', $method->invoke($header, $class, 'Self.isTest.Nice'));
}
}
class GridFieldFilterHeaderTest_DataObject extends DataObject implements TestOnly {
private static $db = array(
'Title' => 'Varchar',
'isTest' => 'Boolean',
);
private static $has_one = array(
'Self' => 'GridFieldFilterHeaderTest_DataObject',
);
}

View File

@ -1126,7 +1126,6 @@ class DataObjectTest extends SapphireTest {
}
public function testValidateModelDefinitionsFailsWithArray() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
@ -1134,54 +1133,33 @@ class DataObjectTest extends SapphireTest {
Config::inst()->update('DataObjectTest_Team', 'has_one', array('NotValid' => array('NoArraysAllowed')));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
$method->invoke($object);
}
public function testValidateModelDefinitionsFailsWithIntKey() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'has_many', array(12 => 'DataObjectTest_Player'));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
$method->invoke($object);
}
public function testValidateModelDefinitionsFailsWithIntValue() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'many_many', array('Players' => 12));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
$method->invoke($object);
}
/**
* many_many_extraFields is allowed to have an array value, so shouldn't throw an exception
*/
public function testValidateModelDefinitionsPassesWithExtraFields() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
@ -1191,12 +1169,9 @@ class DataObjectTest extends SapphireTest {
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest();
$this->fail('Exception should not be thrown');
throw $e;
}
Config::unnest();
}
public function testNewClassInstance() {

View File

@ -37,6 +37,13 @@ class VersionableExtensionsTest extends SapphireTest
parent::setUpOnce();
}
public function tearDownOnce()
{
parent::tearDownOnce();
Config::unnest();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -180,6 +180,45 @@ class MemberAuthenticatorTest extends SapphireTest {
), $form);
$this->assertTrue(Member::default_admin()->isLockedOut());
$this->assertEquals(Member::default_admin()->LockedOutUntil, '2016-04-18 00:10:00');
$this->assertEquals('2016-04-18 00:10:00', Member::default_admin()->LockedOutUntil);
}
public function testNonExistantMemberGetsLoginAttemptRecorded()
{
Config::inst()->update('Member', 'lock_out_after_incorrect_logins', 1);
$email = 'notreal@example.com';
$this->assertFalse(Member::get()->filter(array('Email' => $email))->exists());
$this->assertCount(0, LoginAttempt::get());
$response = MemberAuthenticator::authenticate(array(
'Email' => $email,
'Password' => 'password',
));
$this->assertNull($response);
$this->assertCount(1, LoginAttempt::get());
$attempt = LoginAttempt::get()->first();
$this->assertEquals($email, $attempt->Email);
$this->assertEquals('Failure', $attempt->Status);
}
public function testNonExistantMemberGetsLockedOut()
{
Config::inst()->update('Member', 'lock_out_after_incorrect_logins', 1);
Config::inst()->update('Member', 'lock_out_delay_mins', 10);
$email = 'notreal@example.com';
$this->assertFalse(Member::get()->filter(array('Email' => $email))->exists());
$response = MemberAuthenticator::authenticate(array(
'Email' => $email,
'Password' => 'password'
));
$this->assertNull($response);
$member = new Member();
$member->Email = $email;
$this->assertTrue($member->isLockedOut());
$this->assertFalse($member->canLogIn()->valid());
}
}

View File

@ -846,7 +846,6 @@ class MemberTest extends FunctionalTest {
public function testFailedLoginCount() {
$maxFailedLoginsAllowed = 3;
//set up the config variables to enable login lockouts
Config::nest();
Config::inst()->update('Member', 'lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
$member = $this->objFromFixture('Member', 'test');

View File

@ -75,8 +75,6 @@ class SecurityTest extends FunctionalTest {
}
public function testPermissionFailureSetsCorrectFormMessages() {
Config::nest();
// Controller that doesn't attempt redirections
$controller = new SecurityTest_NullController();
$controller->setResponse(new SS_HTTPResponse());
@ -111,8 +109,6 @@ class SecurityTest extends FunctionalTest {
array('default' => 'default', 'alreadyLoggedIn' => 'One-off failure message'));
$this->assertContains('One-off failure message', $controller->getResponse()->getBody(),
"Message set passed to Security::permissionFailure() didn't override Config values");
Config::unnest();
}
/**
@ -402,6 +398,7 @@ class SecurityTest extends FunctionalTest {
public function testRepeatedLoginAttemptsLockingPeopleOut() {
$local = i18n::get_locale();
i18n::set_locale('en_US');
SS_Datetime::set_mock_now(DBField::create_field('SS_Datetime', '2017-05-22 00:00:00'));
Member::config()->lock_out_after_incorrect_logins = 5;
Member::config()->lock_out_delay_mins = 15;
@ -418,10 +415,9 @@ class SecurityTest extends FunctionalTest {
);
$this->assertContains($this->loginErrorMessage(), Convert::raw2xml(_t('Member.ERRORWRONGCRED')));
} else {
// Fuzzy matching for time to avoid side effects from slow running tests
$this->assertGreaterThan(
time() + 14*60,
strtotime($member->LockedOutUntil),
$this->assertEquals(
SS_Datetime::now()->Format('U') + (15 * 60),
$member->dbObject('LockedOutUntil')->Format('U'),
'User has a lockout time set after too many failed attempts'
);
}
@ -444,14 +440,12 @@ class SecurityTest extends FunctionalTest {
'The user can\'t log in after being locked out, even with the right password'
);
// (We fake this by re-setting LockedOutUntil)
$member = DataObject::get_by_id("Member", $this->idFromFixture('Member', 'test'));
$member->LockedOutUntil = date('Y-m-d H:i:s', time() - 30);
$member->write();
// Move into the future so we can login again
SS_Datetime::set_mock_now(DBField::create_field('SS_Datetime', '2017-06-22 00:00:00'));
$this->doTestLoginForm('testuser@example.com' , '1nitialPassword');
$this->assertEquals(
$this->session()->inst_get('loggedInAs'),
$member->ID,
$this->session()->inst_get('loggedInAs'),
'After lockout expires, the user can login again'
);
@ -471,8 +465,8 @@ class SecurityTest extends FunctionalTest {
$this->doTestLoginForm('testuser@example.com' , '1nitialPassword');
$this->assertEquals(
$this->session()->inst_get('loggedInAs'),
$member->ID,
$this->session()->inst_get('loggedInAs'),
'The user can login successfully after lockout expires, if staying below the threshold'
);

View File

@ -869,10 +869,10 @@ class Requirements_Backend {
// Forcefully put the scripts at the bottom of the body instead of before the first
// script tag.
$replacements["/(<\/body[^>]*>)/i"] = $jsRequirements . "\\1";
$replacements["/(<\/body[^>]*>)/i"] = $this->escapeReplacement($jsRequirements) . "\\1";
// Put CSS at the bottom of the head
$replacements["/(<\/head>)/i"] = $requirements . "\\1";
$replacements["/(<\/head>)/i"] = $this->escapeReplacement($requirements) . "\\1";
} elseif ($this->write_js_to_body) {
$jsRequirements = $this->removeNewlinesFromCode($jsRequirements);
@ -894,14 +894,14 @@ class Requirements_Backend {
if ($canWriteToBody) {
$content = substr($content, 0, $p1) . $jsRequirements . substr($content, $p1);
} else {
$replacements["/(<\/body[^>]*>)/i"] = $jsRequirements . "\\1";
$replacements["/(<\/body[^>]*>)/i"] = $this->escapeReplacement($jsRequirements) . "\\1";
}
// Put CSS at the bottom of the head
$replacements["/(<\/head>)/i"] = $requirements . "\\1";
$replacements["/(<\/head>)/i"] = $this->escapeReplacement($requirements) . "\\1";
} else {
// Put CSS and Javascript together before the closing head tag
$replacements["/(<\/head>)/i"] = $requirements . $jsRequirements. "\\1";
$replacements["/(<\/head>)/i"] = $this->escapeReplacement($requirements . $jsRequirements) . "\\1";
}
if (!empty($replacements)) {
@ -923,6 +923,16 @@ class Requirements_Backend {
return preg_replace('/>\n*/', '>', $code);
}
/**
* Safely escape a literal string for use in preg_replace replacement
*
* @param string $replacement
* @return string
*/
protected function escapeReplacement($replacement) {
return addcslashes($replacement, '\\$');
}
/**
* Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
* HTTP Response