Merge branch '4.2' into 4

This commit is contained in:
Robbie Averill 2018-10-03 15:28:05 +02:00
commit ee24413c30
13 changed files with 266 additions and 14 deletions

View File

@ -94,7 +94,6 @@ SilverStripe core environment variables are listed here, though you're free to d
| `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching). Must be a CacheInterface or CacheFactory class name |
| `SS_IGNORE_DOT_ENV` | If set the .env file will be ignored. This is good for live to mitigate any performance implications of loading the .env file |
| `SS_BASE_URL` | The url to use when it isn't determinable by other means (eg: for CLI commands) |
| `SS_CONFIGSTATICMANIFEST` | Set to `SS_ConfigStaticManifest_Reflection` to use the Silverstripe 4 Reflection config manifest (speed improvement during dev/build and ?flush) |
| `SS_DATABASE_SSL_KEY` | Absolute path to SSL key file |
| `SS_DATABASE_SSL_CERT` | Absolute path to SSL certificate file |
| `SS_DATABASE_SSL_CA` | Absolute path to SSL Certificate Authority bundle file |

View File

@ -22,6 +22,8 @@ class HTTPResponse
protected static $status_codes = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
@ -29,6 +31,9 @@ class HTTPResponse
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
@ -38,6 +43,7 @@ class HTTPResponse
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
@ -53,14 +59,27 @@ class HTTPResponse
415 => 'Unsupported Media Type',
416 => 'Request Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a Teapot',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
451 => 'Unavailable For Legal Reasons',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Unsufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required',
];
/**

View File

@ -105,6 +105,24 @@ class GridField extends FormField
*/
protected $name = '';
/**
* A whitelist of readonly component classes allowed if performReadonlyTransform is called.
*
* @var array
*/
protected $readonlyComponents = array(
GridField_ActionMenu::class,
GridState_Component::class,
GridFieldConfig_RecordViewer::class,
GridFieldDetailForm::class,
GridFieldDataColumns::class,
GridFieldPageCount::class,
GridFieldPaginator::class,
GridFieldSortableHeader::class,
GridFieldToolbarHeader::class,
GridFieldViewButton::class,
);
/**
* Pattern used for looking up
*/
@ -193,6 +211,60 @@ class GridField extends FormField
);
}
/**
* Overload the readonly components for this gridfield.
*
* @param array $components an array map of component class references to whitelist for a readonly version.
*/
public function setReadonlyComponents(array $components)
{
$this->readonlyComponents = $components;
}
/**
* Return the readonly components
*
* @return array a map of component classes.
*/
public function getReadonlyComponents()
{
return $this->readonlyComponents;
}
/**
* Custom Readonly transformation to remove actions which shouldn't be present for a readonly state.
*
* @return GridField
*/
public function performReadonlyTransformation()
{
$copy = clone $this;
$copy->setReadonly(true);
// get the whitelist for allowable readonly components
$allowedComponents = $this->getReadonlyComponents();
foreach ($this->getConfig()->getComponents() as $component) {
// if a component doesn't exist, remove it from the readonly version.
if (!in_array(get_class($component), $allowedComponents)) {
$copy->getConfig()->removeComponent($component);
}
}
return $copy;
}
/**
* Disabling the gridfield should have the same affect as making it readonly (removing all action items).
*
* @return GridField
*/
public function performDisabledTransformation()
{
parent::performDisabledTransformation();
return $this->performReadonlyTransformation();
}
/**
* @return GridFieldConfig
*/

View File

@ -14,6 +14,7 @@ class GridFieldConfig_RecordViewer extends GridFieldConfig_Base
$this->addComponent(new GridFieldViewButton());
$this->addComponent(new GridFieldDetailForm());
$this->removeComponentsByType(GridFieldFilterHeader::class);
$this->extend('updateConfig');
}

View File

@ -103,7 +103,12 @@ class TreeMultiselectField extends TreeDropdownField
// cannot rely on $this->value as this could be a many-many relationship
$value = array_column($values, 'id');
$data['value'] = ($value) ? $value : 'unchanged';
if ($value) {
sort($value);
$data['value'] = $value;
} else {
$data['value'] = 'unchanged';
}
return $data;
}
@ -182,6 +187,7 @@ class TreeMultiselectField extends TreeDropdownField
}
$title = implode(", ", $titleArray);
sort($idArray);
$value = implode(",", $idArray);
} else {
$title = $emptyTitle;

View File

@ -599,7 +599,7 @@ class DataQuery
/**
* Append a HAVING clause to this query.
*
* @param string $having Escaped SQL statement
* @param mixed $having Predicate(s) to set, as escaped SQL statements or parameterised queries
* @return $this
*/
public function having($having)

View File

@ -481,7 +481,7 @@ class SQLSelect extends SQLConditionalExpression
*
* @see SQLSelect::addWhere() for syntax examples
*
* @param mixed $having Predicate(s) to set, as escaped SQL statements or paramaterised queries
* @param mixed $having Predicate(s) to set, as escaped SQL statements or parameterised queries
* @param mixed $having,... Unlimited additional predicates
* @return $this Self reference
*/
@ -497,7 +497,7 @@ class SQLSelect extends SQLConditionalExpression
*
* @see SQLSelect::addWhere() for syntax examples
*
* @param mixed $having Predicate(s) to set, as escaped SQL statements or paramaterised queries
* @param mixed $having Predicate(s) to set, as escaped SQL statements or parameterised queries
* @param mixed $having,... Unlimited additional predicates
* @return $this Self reference
*/

View File

@ -46,6 +46,10 @@ class LoginAttempt extends DataObject
'Member' => Member::class, // only linked if the member actually exists
);
private static $indexes = array(
"EmailHashed" => true
);
private static $table_name = "LoginAttempt";
/**
@ -86,7 +90,6 @@ class LoginAttempt extends DataObject
public static function getByEmail($email)
{
return static::get()->filterAny(array(
'Email' => $email,
'EmailHashed' => sha1($email),
));
}

View File

@ -268,7 +268,7 @@ class Member extends DataObject
public function populateDefaults()
{
parent::populateDefaults();
$this->Locale = i18n::get_locale();
$this->Locale = i18n::config()->get('default_locale');
}
public function requireDefaultRecords()
@ -930,7 +930,7 @@ class Member extends DataObject
// save locale
if (!$this->Locale) {
$this->Locale = i18n::get_locale();
$this->Locale = i18n::config()->get('default_locale');
}
parent::onBeforeWrite();
@ -1229,7 +1229,7 @@ class Member extends DataObject
/**
* Return the date format based on the user's chosen locale,
* falling back to the default format defined by the {@link i18n.get_locale()} setting.
* falling back to the default format defined by the i18n::config()->get('default_locale') config setting.
*
* @return string ISO date format
*/
@ -1248,7 +1248,7 @@ class Member extends DataObject
}
/**
* Get user locale
* Get user locale, falling back to the configured default locale
*/
public function getLocale()
{
@ -1257,12 +1257,12 @@ class Member extends DataObject
return $locale;
}
return i18n::get_locale();
return i18n::config()->get('default_locale');
}
/**
* Return the time format based on the user's chosen locale,
* falling back to the default format defined by the {@link i18n.get_locale()} setting.
* falling back to the default format defined by the i18n::config()->get('default_locale') config setting.
*
* @return string ISO date format
*/

View File

@ -8,7 +8,6 @@ use SilverStripe\Control\HTTPResponse_Exception;
class HTTPResponseTest extends SapphireTest
{
public function testStatusDescriptionStripsNewlines()
{
$r = new HTTPResponse('my body', 200, "my description \nwith newlines \rand carriage returns");
@ -30,7 +29,6 @@ class HTTPResponseTest extends SapphireTest
public function testExceptionContentPlainByDefault()
{
// Confirm that the exception's statusCode and statusDescription take precedence
$e = new HTTPResponse_Exception("Some content that may be from a hacker", 404, 'not even found');
$this->assertEquals("text/plain", $e->getResponse()->getHeader("Content-Type"));
@ -46,4 +44,26 @@ class HTTPResponseTest extends SapphireTest
$response->removeHeader('X-Animal');
$this->assertEmpty($response->getHeader('X-Animal'));
}
public function providerTestValidStatusCodes()
{
return [
[200, 'OK'],
[226, 'IM Used'],
[426, 'Upgrade Required'],
[451, 'Unavailable For Legal Reasons'],
];
}
/**
* @dataProvider providerTestValidStatusCodes
* @param int $code
* @param string $status
*/
public function testValidStatusCodes($code, $status)
{
$response = new HTTPResponse();
$response->setStatusCode($code);
$this->assertEquals($status, $response->getStatusDescription());
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace SilverStripe\Forms\Tests\GridField;
use SilverStripe\Forms\GridField\GridField_ActionMenu;
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
use SilverStripe\Forms\GridField\GridFieldAddNewButton;
use SilverStripe\Forms\GridField\GridFieldButtonRow;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldEditButton;
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
use SilverStripe\Forms\GridField\GridFieldPageCount;
use SilverStripe\Forms\GridField\GridFieldPaginator;
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Versioned\VersionedGridFieldState\VersionedGridFieldState;
class GridFieldReadonlyTest extends SapphireTest
{
protected static $fixture_file = 'GridFieldReadonlyTest.yml';
protected static $extra_dataobjects = array(
Team::class,
Cheerleader::class,
);
/**
* The CMS can set the value of a GridField to be a hasMany relation, which needs a readonly state.
* This test ensures GridField has a readonly transformation.
*/
public function testReadOnlyTransformation()
{
// Build a hasMany Relation via getComponents like ModelAdmin does.
$components = Team::get_one(Team::class)
->getComponents('Cheerleaders');
$gridConfig = GridFieldConfig_RelationEditor::create();
// Build some commonly used components to make sure we're only allowing the correct components
$gridConfig->addComponent(new GridFieldButtonRow('before'));
$gridConfig->addComponent(new GridFieldAddNewButton('buttons-before-left'));
$gridConfig->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-right'));
$gridConfig->addComponent(new GridFieldToolbarHeader());
$gridConfig->addComponent($sort = new GridFieldSortableHeader());
$gridConfig->addComponent($filter = new GridFieldFilterHeader());
$gridConfig->addComponent(new GridFieldDataColumns());
$gridConfig->addComponent(new GridFieldEditButton());
$gridConfig->addComponent(new GridFieldDeleteAction(true));
$gridConfig->addComponent(new GridField_ActionMenu());
$gridConfig->addComponent(new GridFieldPageCount('toolbar-header-right'));
$gridConfig->addComponent($pagination = new GridFieldPaginator(2));
$gridConfig->addComponent(new GridFieldDetailForm());
$gridConfig->addComponent(new GridFieldDeleteAction());
$gridConfig->addComponent(new VersionedGridFieldState());
$gridField = GridField::create(
'Cheerleaders',
'Cheerleaders',
$components,
$gridConfig
);
// Model Admin sets the value of the GridField directly to the relation, which doesn't have a forTemplate()
// function, if we rely on FormField to render into a ReadonlyField we'll get an error as HasManyRelation
// doesn't have a forTemplate() function.
$gridField->setValue($components);
$gridField->setModelClass(Cheerleader::class);
// This function is called by $form->makeReadonly().
$readonlyGridField = $gridField->performReadonlyTransformation();
// if we've made it this far, then the GridField is at least transforming correctly.
$readonlyComponents = $readonlyGridField->getReadonlyComponents();
// assert that all the components in the readonly version are present in the whitelist.
foreach ($readonlyGridField->getConfig()->getComponents() as $component) {
$this->assertTrue(in_array(get_class($component), $readonlyComponents));
}
}
}

View File

@ -0,0 +1,27 @@
SilverStripe\Forms\Tests\GridField\GridFieldTest\Team:
team1:
Name: Team 1
City: Cologne
team2:
Name: Team 2
City: Wellington
team3:
Name: Team 3
City: Auckland
team4:
Name: Team 4
City: Melbourne
SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader:
cheerleader1:
Name: Heather
Team: =>SilverStripe\Forms\Tests\GridField\GridFieldTest\Team.team1
cheerleader2:
Name: Bob
Team: =>SilverStripe\Forms\Tests\GridField\GridFieldTest\Team.team1
cheerleader3:
Name: Jenny
Team: =>SilverStripe\Forms\Tests\GridField\GridFieldTest\Team.team1
cheerleader4:
Name: Sam
Team: =>SilverStripe\Forms\Tests\GridField\GridFieldTest\Team.team1

View File

@ -56,6 +56,8 @@ class MemberTest extends FunctionalTest
Member::config()->set('unique_identifier_field', 'Email');
Member::set_password_validator(null);
i18n::set_locale('en_US');
}
public function testPasswordEncryptionUpdatedOnChangedPassword()
@ -1533,4 +1535,20 @@ class MemberTest extends FunctionalTest
$this->assertInstanceOf(ValidationResult::class, $result);
$this->assertFalse($result->isValid());
}
public function testNewMembersReceiveTheDefaultLocale()
{
// Set a different current locale to the default
i18n::set_locale('de_DE');
$newMember = Member::create();
$newMember->update([
'FirstName' => 'Leslie',
'Surname' => 'Longly',
'Email' => 'longly.leslie@example.com',
]);
$newMember->write();
$this->assertSame('en_US', $newMember->Locale, 'New members receive the default locale');
}
}