Merge branch '4.3' into 4

# Conflicts:
 #	tests/php/Forms/ConfirmedPasswordFieldTest.php
This commit is contained in:
Robbie Averill 2018-11-26 12:15:17 +01:00
commit 1f1c344272
19 changed files with 687 additions and 94 deletions

13
_config/passwords.yml Normal file
View File

@ -0,0 +1,13 @@
---
Name: corepasswords
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Security\PasswordValidator:
properties:
MinLength: 8
HistoricCount: 6
# In the case someone uses `new PasswordValidator` instead of Injector, provide some safe defaults through config.
SilverStripe\Security\PasswordValidator:
min_length: 8
historic_count: 6

View File

@ -838,7 +838,420 @@ $obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
Versioned::set_reading_mode($origMode); // reset current mode Versioned::set_reading_mode($origMode); // reset current mode
``` ```
## Using the history viewer
Since SilverStripe 4.3 you can use the React and GraphQL driven history viewer UI to display historic changes and
comparisons for a versioned DataObject. This is automatically enabled for SiteTree objects and content blocks in
[dnadesign/silverstripe-elemental](https://github.com/dnadesign/silverstripe-elemental).
If you want to enable the history viewer for a custom versioned DataObject, you will need to:
* Expose GraphQL scaffolding
* Add the necessary GraphQL queries and mutations to your module
* Register your GraphQL queries and mutations with Injector
* Add a HistoryViewerField to the DataObject's `getCMSFields`
**Please note:** these examples are given in the context of project-level customisation. You may need to adjust
the webpack configuration slightly for use in a module. They are also designed to be used on SilverStripe 4.3 or
later.
For these examples, you can use this simple DataObject and create a ModelAdmin for it:
```php
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
class MyVersionedObject extends DataObject
{
private static $db = [
'Title' => 'Varchar',
];
private static $extensions = [
Versioned::class,
];
}
```
### Configure frontend asset building
If you haven't already configured frontend asset building for your project, you will need to configure some basic
packages to be built via webpack in order to enable history viewer functionality. If you have this configured for
your project already, ensure you have the `react-apollo` and `graphql-tag` libraries in your `package.json`
requirements, and skip this section.
You can configure your directory structure like so:
**package.json**
```json
{
"name": "my-project",
"scripts": {
"build": "yarn && NODE_ENV=production webpack -p --bail --progress",
"watch": "yarn && NODE_ENV=development webpack --watch --progress"
},
"dependencies": {
"react-apollo": "^0.7.1",
"graphql-tag": "^0.1.17"
},
"devDependencies": {
"@silverstripe/webpack-config": "^0.4.1",
"webpack": "^2.6.1"
},
"jest": {
"roots": [
"client/src"
],
"moduleDirectories": [
"app/client/src",
"node_modules",
"node_modules/@silverstripe/webpack-config/node_modules",
"vendor/silverstripe/admin/client/src",
"vendor/silverstripe/admin/node_modules"
]
},
"babel": {
"presets": [
"env",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
},
"engines": {
"node": "^6.x"
}
}
```
**webpack.config.js**
```json
const Path = require('path');
// Import the core config
const webpackConfig = require('@silverstripe/webpack-config');
const {
resolveJS,
externalJS,
moduleJS,
pluginJS,
} = webpackConfig;
const ENV = process.env.NODE_ENV;
const PATHS = {
MODULES: 'node_modules',
FILES_PATH: '../',
ROOT: Path.resolve(),
SRC: Path.resolve('app/client/src'),
DIST: Path.resolve('app/client/dist'),
};
const config = [
{
name: 'js',
entry: {
bundle: `${PATHS.SRC}/boot/index.js`,
},
output: {
path: PATHS.DIST,
filename: 'js/[name].js',
},
devtool: (ENV !== 'production') ? 'source-map' : '',
resolve: resolveJS(ENV, PATHS),
externals: externalJS(ENV, PATHS),
module: moduleJS(ENV, PATHS),
plugins: pluginJS(ENV, PATHS),
}
];
// Use WEBPACK_CHILD=js or WEBPACK_CHILD=css env var to run a single config
module.exports = (process.env.WEBPACK_CHILD)
? config.find((entry) => entry.name === process.env.WEBPACK_CHILD)
: module.exports = config;
```
**composer.json**
```json
"extra": {
"expose": [
"app/client/dist"
]
}
```
**app/client/src/boot/index.js**
```js
console.log('Hello world');
```
**.eslintrc.js**
```js
module.exports = require('@silverstripe/webpack-config/.eslintrc');
```
At this stage, running `yarn build` should show you a linting warning for the console statement, and correctly build
`app/client/dist/js/bundle.js`.
### Expose GraphQL scaffolding
Only a minimal amount of data is required to be exposed via GraphQL scaffolding, and only to the "admin" GraphQL
schema. For more information, see [ReactJS, Redux and GraphQL](../../customising_the_admin_interface/react_redux_and_graphql).
**app/_config/graphql.yml**
```yaml
SilverStripe\GraphQL\Manager:
schemas:
admin:
scaffolding:
types:
MyVersionedObject:
fields: [ID, LastEdited]
operations:
readOne: true
SilverStripe\Security\Member:
fields: [ID, FirstName, Surname]
operations:
readOne: true
```
Once configured, flush your cache and explore the new GraphQL schema to ensure it loads correctly. You can use a GraphQL
application such as GraphiQL, or [silverstripe-graphql-devtools](https://github.com/silverstripe/silverstripe-graphql-devtools)
for a browser solution:
```
composer require --dev silverstripe/graphql-devtools dev-master
```
### Configure the necessary GraphQL queries and mutations
The history viewer interface uses two main operations:
* Read a list of versions for a DataObject
* Revert to an older version of a DataObject
For this we need one query and one mutation:
**app/client/src/state/readOneMyVersionedObjectQuery.js**
```js
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
// GraphQL query for retrieving the version history of a specific object. The results of
// the query must be set to the "versions" prop on the component that this HOC is
// applied to for binding implementation.
const query = gql`
query ReadHistoryViewerMyVersionedObject ($id: ID!, $limit: Int!, $offset: Int!) {
readOneMyVersionedObject(
Versioning: {
Mode: LATEST
},
ID: $id
) {
ID
Versions (limit: $limit, offset: $offset) {
pageInfo {
totalCount
}
edges {
node {
Version
Author {
FirstName
Surname
}
Publisher {
FirstName
Surname
}
Published
LiveVersion
LatestDraftVersion
LastEdited
}
}
}
}
}
`;
const config = {
options({recordId, limit, page}) {
return {
variables: {
limit,
offset: ((page || 1) - 1) * limit,
block_id: recordId,
}
};
},
props(
{
data: {
error,
refetch,
readOneMyVersionedObject,
loading: networkLoading,
},
ownProps: {
actions = {
versions: {}
},
limit,
recordId,
},
}
) {
const versions = readOneMyVersionedObject || null;
const errors = error && error.graphQLErrors &&
error.graphQLErrors.map((graphQLError) => graphQLError.message);
return {
loading: networkLoading || !versions,
versions,
graphQLErrors: errors,
actions: {
...actions,
versions: {
...versions,
goToPage(page) {
refetch({
offset: ((page || 1) - 1) * limit,
limit,
block_id: recordId,
});
}
},
},
};
},
};
export { query, config };
export default graphql(query, config);
```
**app/client/src/state/revertToMyVersionedObjectVersionMutation.js**
```js
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const mutation = gql`
mutation revertMyVersionedObjectToVersion($id:ID!, $toVersion:Int!) {
rollbackMyVersionedObject(
ID: $id
ToVersion: $toVersion
) {
ID
}
}
`;
const config = {
props: ({ mutate, ownProps: { actions } }) => {
const revertToVersion = (id, toVersion) => mutate({
variables: {
id,
toVersion,
},
});
return {
actions: {
...actions,
revertToVersion,
},
};
},
options: {
// Refetch versions after mutation is completed
refetchQueries: ['ReadHistoryViewerMyVersionedObject']
}
};
export { mutation, config };
export default graphql(mutation, config);
````
### Register your GraphQL query and mutation with Injector
Once your GraphQL query and mutation are created, you will need to tell the JavaScript Injector about them.
This does two things:
* Allow them to be loaded by core components.
* Allow Injector to provide them in certain contexts. They should be available for `MyVersionedObject` history viewer
instances, but not for CMS pages for example.
**app/client/src/boot/index.js**
```js
/* global window */
import Injector from 'lib/Injector';
import readOneMyVersionedObjectQuery from 'state/readOneMyVersionedObjectQuery';
import revertToMyVersionedObjectVersionMutation from 'state/revertToMyVersionedObjectVersionMutation';
window.document.addEventListener('DOMContentLoaded', () => {
// Register GraphQL operations with Injector as transformations
Injector.transform(
'myversionedobject-history', (updater) => {
updater.component(
'HistoryViewer.Form_ItemEditForm',
readOneMyVersionedObjectQuery, 'ElementHistoryViewer');
}
);
Injector.transform(
'myversionedobject-history-revert', (updater) => {
updater.component(
'HistoryViewerToolbar.VersionedAdmin.HistoryViewer.MyVersionedObject.HistoryViewerVersionDetail',
revertToMyVersionedObjectVersionMutation,
'MyVersionedObjectRevertMutation'
);
}
);
});
```
For more information, see [ReactJS, Redux and GraphQL](../../customising_the_admin_interface/react_redux_and_graphql).
### Adding the HistoryViewerField
You can add the [HistoryViewerField](api:SilverStripe\VersionedAdmin\Forms\HistoryViewerField) to your object's CMS
fields in the same way as any other form field:
```php
use SilverStripe\VersionedAdmin\Forms\HistoryViewerField;
use SilverStripe\View\Requirements;
public function getCMSFields()
{
$fields = parent::getCMSFields();
Requirements::javascript('app/client/dist/js/bundle.js');
$fields->addFieldToTab('Root.History', HistoryViewerField::create('MyObjectHistory'));
return $fields;
}
```
### Previewable DataObjects
History viewer will automatically detect and render a side-by-side preview panel for DataObjects that implement
[CMSPreviewable](api:SilverStripe\ORM\CMSPreviewable). Please note that if you are adding this functionality, you
will also need to expose the `AbsoluteLink` field in your GraphQL read scaffolding, and add it to the fields in
`readOneMyVersionedObjectQuery`.
## API Documentation ## API Documentation
* [Versioned](api:SilverStripe\Versioned\Versioned) * [Versioned](api:SilverStripe\Versioned\Versioned)
* [HistoryViewerField](api:SilverStripe\VersionedAdmin\Forms\HistoryViewerField)

View File

@ -56,7 +56,7 @@ Alternatively, we can add extensions through PHP code (in the `_config.php` file
```php ```php
SilverStripe\Security\Member::add_extension('MyMemberExtension'); SilverStripe\Security\Member::add_extension(MyMemberExtension::class);
``` ```
This class now defines a `MyMemberExtension` that applies to all `Member` instances on the website. It will have This class now defines a `MyMemberExtension` that applies to all `Member` instances on the website. It will have
@ -256,7 +256,7 @@ $member = Security::getCurrentUser();
print_r($member->getExtensionInstances()); print_r($member->getExtensionInstances());
if($member->hasExtension('MyCustomMemberExtension')) { if ($member->hasExtension(MyCustomMemberExtension::class)) {
// .. // ..
} }
``` ```
@ -282,7 +282,7 @@ if not specified in `self::$defaults`, but before extensions have been called:
public function __construct() public function __construct()
{ {
$this->beforeExtending('populateDefaults', function() { $this->beforeExtending('populateDefaults', function() {
if(empty($this->MyField)) { if (empty($this->MyField)) {
$this->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions'; $this->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions';
} }
}); });
@ -301,9 +301,9 @@ This method is preferred to disabling, enabling, and calling field extensions ma
```php ```php
public function getCMSFields() public function getCMSFields()
{ {
$this->beforeUpdateCMSFields(function($fields) { $this->beforeUpdateCMSFields(function ($fields) {
// Include field which must be present when updateCMSFields is called on extensions // Include field which must be present when updateCMSFields is called on extensions
$fields->addFieldToTab("Root.Main", new TextField('Detail', 'Details', null, 255)); $fields->addFieldToTab('Root.Main', new TextField('Detail', 'Details', null, 255));
}); });
$fields = parent::getCMSFields(); $fields = parent::getCMSFields();
@ -312,9 +312,45 @@ public function getCMSFields()
} }
``` ```
## Related Lessons ## Extending extensions {#extendingextensions}
* [DataExtensions and SiteConfig](https://www.silverstripe.org/learn/lessons/v4/data-extensions-and-siteconfig-1)
Extension classes can be overloaded using the Injector, if you want to modify the way that an extension in one of
your modules works:
```yaml
SilverStripe\Core\Injector\Injector:
Company\Vendor\SomeExtension:
class: App\Project\CustomisedSomeExtension
```
**app/src/CustomisedSomeExtension.php**
```php
namespace App\Project;
use Company\Vendor\SomeExtension;
class CustomisedSomeExtension extends SomeExtension
{
public function someMethod()
{
$result = parent::someMethod();
// modify result;
return $result;
}
}
```
<div class="notice" markdown="1">
Please note that modifications such as this should be done in YAML configuration only. It is not recommended
to use `Config::modify()->set()` to adjust the implementation class name of an extension after the configuration
manifest has been loaded, and may not work consistently due to the "extra methods" cache having already been
populated.
</div>
## Related Lessons
* [DataExtensions and SiteConfig](https://www.silverstripe.org/learn/lessons/v4/data-extensions-and-siteconfig-1)
## Related Documentaion ## Related Documentaion

View File

@ -549,23 +549,50 @@ salt values generated with the strongest entropy generators available on the pla
(see [RandomGenerator](api:SilverStripe\Security\RandomGenerator)). This prevents brute force attacks with (see [RandomGenerator](api:SilverStripe\Security\RandomGenerator)). This prevents brute force attacks with
[Rainbow tables](http://en.wikipedia.org/wiki/Rainbow_table). [Rainbow tables](http://en.wikipedia.org/wiki/Rainbow_table).
Strong passwords are a crucial part of any system security. Strong passwords are a crucial part of any system security. So in addition to storing the password in a secure fashion,
So in addition to storing the password in a secure fashion, you can also enforce specific password policies by configuring a
you can also enforce specific password policies by configuring [PasswordValidator](api:SilverStripe\Security\PasswordValidator). This can be done through a `_config.php` file
a [PasswordValidator](api:SilverStripe\Security\PasswordValidator): at runtime, or via YAML configuration.
From SilverStripe 4.3 onwards, the default password validation rules are configured in the framework's `passwords.yml`
file. You will need to ensure that your config file is processed after it. For SilverStripe <4.3 you will need to
use a `_config.php` file to modify the class's config at runtime (see `_config.php` installed in your mysite/app folder
if you're using silverstripe/recipe-core).
```php ```yaml
use SilverStripe\Security\Member; ---
use SilverStripe\Security\PasswordValidator; Name: mypasswords
After: '#corepasswords'
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Security\PasswordValidator:
properties:
MinLength: 7
HistoricCount: 6
MinTestScore: 3
$validator = new PasswordValidator(); # In the case someone uses `new PasswordValidator` instead of Injector, provide some safe defaults through config.
$validator->minLength(7); SilverStripe\Security\PasswordValidator:
$validator->checkHistoricalPasswords(6); min_length: 7
$validator->characterStrength(3, ["lowercase", "uppercase", "digits", "punctuation"]); historic_count: 6
Member::set_password_validator($validator); min_test_score: 3
``` ```
### Configuring custom password validator tests
The default password validation character strength tests can be seen in the `PasswordValidator.character_strength_tests`
configuration property. You can add your own with YAML config, by providing a name for it and a regex pattern to match:
```yaml
SilverStripe\Security\PasswordValidator:
character_strength_tests:
contains_secret_word: '/1337pw/'
```
This will ensure that a password contains `1337pw` somewhere in the string before validation will succeed.
### Other options
In addition, you can tighten password security with the following configuration settings: In addition, you can tighten password security with the following configuration settings:
* `Member.password_expiry_days`: Set the number of days that a password should be valid for. * `Member.password_expiry_days`: Set the number of days that a password should be valid for.

View File

@ -26,7 +26,7 @@ To enable the legacy search API on a `GridFieldFilterHeader`, you can either:
* set the `useLegacyFilterHeader` property to `true`, * set the `useLegacyFilterHeader` property to `true`,
* or pass `true` to the first argument of its constructor. * or pass `true` to the first argument of its constructor.
To force the legacy search API on all instances of `GridFieldFilterHeader`, you can set it in your [configuration file](../../configuration): To force the legacy search API on all instances of `GridFieldFilterHeader`, you can set it in your [configuration file](../developer_guides/configuration):
```yml ```yml
SilverStripe\Forms\GridField\GridFieldFilterHeader: SilverStripe\Forms\GridField\GridFieldFilterHeader:
force_legacy: true force_legacy: true
@ -67,4 +67,18 @@ SilverStripe\Core\Injector\Injector:
App\MySite\MyCustomControllerFactory App\MySite\MyCustomControllerFactory
``` ```
[Implementing a _Factory_ with the Injector](/developer_guides/extending/injector/#factories) [Implementing a _Factory_ with the Injector](/developer_guides/extending/injector/#factories).
### Using the history viewer for custom DataObjects
For information on how to implement the history viewer UI in your own versioned DataObjects, please refer to
[the Versioning documentation](../developer_guides/model/versioning).
### Tests with dynamic extension customisations
In SilverStripe 4.2, some unit tests that modify an extension class with PHP configuration manifest customisations
may have passed and may now fail in SilverStripe 4.3. This behaviour is inconsistent, is not a recommended approach
to customising extensions and should be avoided in all SilverStripe 4.x releases.
For information on how to customise extensions, see
["Extending Extensions"](../developer_guides/extending/extensions#extendingextensions).

View File

@ -215,15 +215,6 @@ trait Extensible
return true; return true;
} }
/**
* Clears all cached extra_methods cache data
*/
public static function flush_extra_methods_cache()
{
self::$extra_methods = [];
}
/** /**
* Remove an extension from a class. * Remove an extension from a class.
* Note: This will not remove extensions from parent classes, and must be called * Note: This will not remove extensions from parent classes, and must be called

View File

@ -139,9 +139,10 @@ class ConfirmationTokenChain
*/ */
public function getRedirectUrlParams() public function getRedirectUrlParams()
{ {
$params = []; $params = $_GET;
unset($params['url']); // CLIRequestBuilder may add this
foreach ($this->filteredTokens() as $token) { foreach ($this->filteredTokens() as $token) {
$params = array_merge($params, $token->getRedirectUrlParams()); $params = array_merge($params, $token->params());
} }
return $params; return $params;

View File

@ -13,6 +13,16 @@ use SilverStripe\ORM\DataObject;
*/ */
class ExtensionTestState implements TestState class ExtensionTestState implements TestState
{ {
/**
* @var array
*/
protected $extensionsToReapply = [];
/**
* @var array
*/
protected $extensionsToRemove = [];
/** /**
* Called on setup * Called on setup
* *
@ -20,7 +30,6 @@ class ExtensionTestState implements TestState
*/ */
public function setUp(SapphireTest $test) public function setUp(SapphireTest $test)
{ {
DataObject::flush_extra_methods_cache();
} }
public function tearDown(SapphireTest $test) public function tearDown(SapphireTest $test)
@ -31,6 +40,8 @@ class ExtensionTestState implements TestState
{ {
// May be altered by another class // May be altered by another class
$isAltered = false; $isAltered = false;
$this->extensionsToReapply = [];
$this->extensionsToRemove = [];
/** @var string|SapphireTest $class */ /** @var string|SapphireTest $class */
/** @var string|DataObject $dataClass */ /** @var string|DataObject $dataClass */
@ -46,6 +57,10 @@ class ExtensionTestState implements TestState
if (!class_exists($extension) || !$dataClass::has_extension($extension)) { if (!class_exists($extension) || !$dataClass::has_extension($extension)) {
continue; continue;
} }
if (!isset($this->extensionsToReapply[$dataClass])) {
$this->extensionsToReapply[$dataClass] = [];
}
$this->extensionsToReapply[$dataClass][] = $extension;
$dataClass::remove_extension($extension); $dataClass::remove_extension($extension);
$isAltered = true; $isAltered = true;
} }
@ -62,6 +77,10 @@ class ExtensionTestState implements TestState
throw new LogicException("Test {$class} requires extension {$extension} which doesn't exist"); throw new LogicException("Test {$class} requires extension {$extension} which doesn't exist");
} }
if (!$dataClass::has_extension($extension)) { if (!$dataClass::has_extension($extension)) {
if (!isset($this->extensionsToRemove[$dataClass])) {
$this->extensionsToRemove[$dataClass] = [];
}
$this->extensionsToRemove[$dataClass][] = $extension;
$dataClass::add_extension($extension); $dataClass::add_extension($extension);
$isAltered = true; $isAltered = true;
} }
@ -85,6 +104,23 @@ class ExtensionTestState implements TestState
public function tearDownOnce($class) public function tearDownOnce($class)
{ {
DataObject::flush_extra_methods_cache(); // @todo: This isn't strictly necessary to restore extensions, but only to ensure that
// Object::$extra_methods is properly flushed. This should be replaced with a simple
// flush mechanism for each $class.
/** @var string|DataObject $dataClass */
// Remove extensions added for testing
foreach ($this->extensionsToRemove as $dataClass => $extensions) {
foreach ($extensions as $extension) {
$dataClass::remove_extension($extension);
}
}
// Reapply ones removed
foreach ($this->extensionsToReapply as $dataClass => $extensions) {
foreach ($extensions as $extension) {
$dataClass::add_extension($extension);
}
}
} }
} }

View File

@ -499,6 +499,17 @@ class TreeDropdownField extends FormField
// Begin marking // Begin marking
$markingSet->markPartialTree(); $markingSet->markPartialTree();
// Explicitely mark our search results if necessary
foreach ($this->searchIds as $id => $marked) {
if ($marked) {
$object = $this->objectForKey($id);
if (!$object) {
continue;
}
$markingSet->markToExpose($object);
}
}
// Allow to pass values to be selected within the ajax request // Allow to pass values to be selected within the ajax request
$value = $request->requestVar('forceValue') ?: $this->value; $value = $request->requestVar('forceValue') ?: $this->value;
if ($value && ($values = preg_split('/,\s*/', $value))) { if ($value && ($values = preg_split('/,\s*/', $value))) {

View File

@ -21,13 +21,12 @@ use SilverStripe\Core\Tests\Injector\InjectorTest\NeedsBothCirculars;
use SilverStripe\Core\Tests\Injector\InjectorTest\NewRequirementsBackend; use SilverStripe\Core\Tests\Injector\InjectorTest\NewRequirementsBackend;
use SilverStripe\Core\Tests\Injector\InjectorTest\OriginalRequirementsBackend; use SilverStripe\Core\Tests\Injector\InjectorTest\OriginalRequirementsBackend;
use SilverStripe\Core\Tests\Injector\InjectorTest\OtherTestObject; use SilverStripe\Core\Tests\Injector\InjectorTest\OtherTestObject;
use SilverStripe\Core\Tests\Injector\InjectorTest\SomeCustomisedExtension;
use SilverStripe\Core\Tests\Injector\InjectorTest\SomeExtension;
use SilverStripe\Core\Tests\Injector\InjectorTest\TestObject; use SilverStripe\Core\Tests\Injector\InjectorTest\TestObject;
use SilverStripe\Core\Tests\Injector\InjectorTest\TestSetterInjections; use SilverStripe\Core\Tests\Injector\InjectorTest\TestSetterInjections;
use SilverStripe\Core\Tests\Injector\InjectorTest\TestStaticInjections; use SilverStripe\Core\Tests\Injector\InjectorTest\TestStaticInjections;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Member; use SilverStripe\Dev\TestOnly;
use stdClass;
define('TEST_SERVICES', __DIR__ . '/AopProxyServiceTest'); define('TEST_SERVICES', __DIR__ . '/AopProxyServiceTest');
@ -1048,24 +1047,4 @@ class InjectorTest extends SapphireTest
Injector::unnest(); Injector::unnest();
$this->nestingLevel--; $this->nestingLevel--;
} }
/**
* Tests that overloaded extensions work, see {@link Extensible::getExtensionInstance()}
*/
public function testExtendedExtensions()
{
Config::modify()
->set(Injector::class, SomeExtension::class, [
'class' => SomeCustomisedExtension::class,
])
->merge(Member::class, 'extensions', [
SomeExtension::class,
]);
/** @var Member|SomeExtension $member */
$member = new Member();
$this->assertTrue($member->hasExtension(SomeExtension::class));
$this->assertTrue($member->hasMethod('someMethod'));
$this->assertSame('bar', $member->someMethod());
}
} }

View File

@ -1,13 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Injector\InjectorTest;
use SilverStripe\Dev\TestOnly;
class SomeCustomisedExtension extends SomeExtension implements TestOnly
{
public function someMethod()
{
return 'bar';
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Injector\InjectorTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataExtension;
class SomeExtension extends DataExtension implements TestOnly
{
public function someMethod()
{
return 'foo';
}
}

View File

@ -167,19 +167,21 @@ class ConfirmationTokenChainTest extends SapphireTest
public function testGetRedirectUrlParams() public function testGetRedirectUrlParams()
{ {
$mockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); $mockToken = $this->getTokenRequiringReload(true, ['params']);
$mockToken->expects($this->once()) $mockToken->expects($this->once())
->method('getRedirectUrlParams') ->method('params')
->will($this->returnValue(['mockTokenParam' => '1'])); ->will($this->returnValue(['mockTokenParam' => '1']));
$secondMockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); $secondMockToken = $this->getTokenRequiringReload(true, ['params']);
$secondMockToken->expects($this->once()) $secondMockToken->expects($this->once())
->method('getRedirectUrlParams') ->method('params')
->will($this->returnValue(['secondMockTokenParam' => '2'])); ->will($this->returnValue(['secondMockTokenParam' => '2']));
$chain = new ConfirmationTokenChain(); $chain = new ConfirmationTokenChain();
$chain->pushToken($mockToken); $chain->pushToken($mockToken);
$chain->pushToken($secondMockToken); $chain->pushToken($secondMockToken);
$this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->getRedirectUrlParams()); $params = $chain->getRedirectUrlParams();
$this->assertEquals('1', $params['mockTokenParam']);
$this->assertEquals('2', $params['secondMockTokenParam']);
} }
} }

View File

@ -48,7 +48,7 @@ class ErrorControlChainMiddlewareTest extends SapphireTest
$this->assertInstanceOf(HTTPResponse::class, $result); $this->assertInstanceOf(HTTPResponse::class, $result);
$location = $result->getHeader('Location'); $location = $result->getHeader('Location');
$this->assertContains('?flush=1&flushtoken=', $location); $this->assertContains('flush=1&flushtoken=', $location);
$this->assertNotContains('Security/login', $location); $this->assertNotContains('Security/login', $location);
} }
@ -95,7 +95,7 @@ class ErrorControlChainMiddlewareTest extends SapphireTest
$this->assertInstanceOf(HTTPResponse::class, $result); $this->assertInstanceOf(HTTPResponse::class, $result);
$location = $result->getHeader('Location'); $location = $result->getHeader('Location');
$this->assertContains('/dev/build', $location); $this->assertContains('/dev/build', $location);
$this->assertContains('?devbuildtoken=', $location); $this->assertContains('devbuildtoken=', $location);
$this->assertNotContains('Security/login', $location); $this->assertNotContains('Security/login', $location);
} }

View File

@ -10,9 +10,19 @@ use SilverStripe\Forms\Form;
use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\ReadonlyField;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\PasswordValidator;
class ConfirmedPasswordFieldTest extends SapphireTest class ConfirmedPasswordFieldTest extends SapphireTest
{ {
protected $usesDatabase = true;
protected function setUp()
{
parent::setUp();
PasswordValidator::singleton()->setMinLength(0);
}
public function testSetValue() public function testSetValue()
{ {
$field = new ConfirmedPasswordField('Test', 'Testing', 'valueA'); $field = new ConfirmedPasswordField('Test', 'Testing', 'valueA');

View File

@ -9,12 +9,17 @@ use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Forms\TreeDropdownField; use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\ORM\Tests\HierarchyTest\TestObject;
class TreeDropdownFieldTest extends SapphireTest class TreeDropdownFieldTest extends SapphireTest
{ {
protected static $fixture_file = 'TreeDropdownFieldTest.yml'; protected static $fixture_file = 'TreeDropdownFieldTest.yml';
protected static $extra_dataobjects = [
TestObject::class
];
public function testSchemaStateDefaults() public function testSchemaStateDefaults()
{ {
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class); $field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
@ -97,6 +102,38 @@ class TreeDropdownFieldTest extends SapphireTest
); );
} }
public function testTreeSearchJsonFlatlistWithLowNodeThreshold()
{
// Initialise our TreeDropDownField
$field = new TreeDropdownField('TestTree', 'Test tree', TestObject::class);
$field->config()->set('node_threshold_total', 2);
// Search for all Test object matching our criteria
$request = new HTTPRequest(
'GET',
'url',
['search' => 'MatchSearchCriteria', 'format' => 'json', 'flatList' => '1']
);
$request->setSession(new Session([]));
$response = $field->tree($request);
$tree = json_decode($response->getBody(), true);
$actualNodeIDs = array_column($tree['children'], 'id');
// Get the list of expected node IDs from the YML Fixture
$expectedNodeIDs = array_map(
function ($key) {
return $this->objFromFixture(TestObject::class, $key)->ID;
},
['zero', 'oneA', 'twoAi', 'three'] // Those are the identifiers of the object we expect our search to find
);
sort($actualNodeIDs);
sort($expectedNodeIDs);
$this->assertEquals($expectedNodeIDs, $actualNodeIDs);
}
public function testTreeSearch() public function testTreeSearch()
{ {
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class); $field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);

View File

@ -8,6 +8,7 @@ SilverStripe\Assets\Folder:
folder1-subfolder1: folder1-subfolder1:
Name: FileTest-folder1-subfolder1 Name: FileTest-folder1-subfolder1
ParentID: =>SilverStripe\Assets\Folder.folder1 ParentID: =>SilverStripe\Assets\Folder.folder1
SilverStripe\Assets\File: SilverStripe\Assets\File:
asdf: asdf:
Filename: assets/FileTest.txt Filename: assets/FileTest.txt
@ -24,3 +25,40 @@ SilverStripe\Assets\File:
Filename: assets/FileTest-folder1/File1.txt Filename: assets/FileTest-folder1/File1.txt
Name: File1.txt Name: File1.txt
ParentID: =>SilverStripe\Assets\Folder.folder1 ParentID: =>SilverStripe\Assets\Folder.folder1
SilverStripe\ORM\Tests\HierarchyTest\TestObject:
zero:
Title: Zero MatchSearchCriteria
zeroA:
Title: Child A of Zero
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.zero
zeroB:
Title: Child B of Zero
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.zero
zeroC:
Title: Child C of Zero
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.zero
one:
Title: One
oneA:
Title: Child A of One MatchSearchCriteria
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.one
oneB:
Title: Child B of One
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.one
oneC:
Title: Child C of One
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.one
oneD:
Title: Child C of One
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.one
two:
Title: Two
twoA:
Title: Child A of Two
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.two
twoAi:
Title: Grandchild i of Child A of Two MatchSearchCriteria
ParentID: =>SilverStripe\ORM\Tests\HierarchyTest\TestObject.twoA
three:
Title: Three MatchSearchCriteria

View File

@ -22,6 +22,7 @@ use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler; use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
use SilverStripe\Security\MemberPassword; use SilverStripe\Security\MemberPassword;
use SilverStripe\Security\PasswordEncryptor_Blowfish; use SilverStripe\Security\PasswordEncryptor_Blowfish;
use SilverStripe\Security\PasswordValidator;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\RememberLoginHash; use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
@ -55,7 +56,8 @@ class MemberTest extends FunctionalTest
parent::setUp(); parent::setUp();
Member::config()->set('unique_identifier_field', 'Email'); Member::config()->set('unique_identifier_field', 'Email');
Member::set_password_validator(null);
PasswordValidator::singleton()->setMinLength(0);
i18n::set_locale('en_US'); i18n::set_locale('en_US');
} }

View File

@ -2,9 +2,9 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Security\PasswordValidator;
use SilverStripe\Security\Member;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Member;
use SilverStripe\Security\PasswordValidator;
class PasswordValidatorTest extends SapphireTest class PasswordValidatorTest extends SapphireTest
{ {
@ -14,6 +14,16 @@ class PasswordValidatorTest extends SapphireTest
*/ */
protected $usesDatabase = true; protected $usesDatabase = true;
protected function setUp()
{
parent::setUp();
// Unset framework default values
PasswordValidator::config()
->remove('min_length')
->remove('historic_count');
}
public function testValidate() public function testValidate()
{ {
$v = new PasswordValidator(); $v = new PasswordValidator();