mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '4.3' into 4
# Conflicts: # tests/php/Forms/ConfirmedPasswordFieldTest.php
This commit is contained in:
commit
1f1c344272
13
_config/passwords.yml
Normal file
13
_config/passwords.yml
Normal 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
|
@ -838,7 +838,420 @@ $obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||
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
|
||||
|
||||
* [Versioned](api:SilverStripe\Versioned\Versioned)
|
||||
* [HistoryViewerField](api:SilverStripe\VersionedAdmin\Forms\HistoryViewerField)
|
||||
|
@ -56,7 +56,7 @@ Alternatively, we can add extensions through PHP code (in the `_config.php` file
|
||||
|
||||
|
||||
```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
|
||||
@ -256,7 +256,7 @@ $member = Security::getCurrentUser();
|
||||
|
||||
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()
|
||||
{
|
||||
$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';
|
||||
}
|
||||
});
|
||||
@ -301,9 +301,9 @@ This method is preferred to disabling, enabling, and calling field extensions ma
|
||||
```php
|
||||
public function getCMSFields()
|
||||
{
|
||||
$this->beforeUpdateCMSFields(function($fields) {
|
||||
$this->beforeUpdateCMSFields(function ($fields) {
|
||||
// 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();
|
||||
@ -312,9 +312,45 @@ public function getCMSFields()
|
||||
}
|
||||
```
|
||||
|
||||
## Related Lessons
|
||||
* [DataExtensions and SiteConfig](https://www.silverstripe.org/learn/lessons/v4/data-extensions-and-siteconfig-1)
|
||||
## Extending extensions {#extendingextensions}
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
[Rainbow tables](http://en.wikipedia.org/wiki/Rainbow_table).
|
||||
|
||||
Strong passwords are a crucial part of any system security.
|
||||
So in addition to storing the password in a secure fashion,
|
||||
you can also enforce specific password policies by configuring
|
||||
a [PasswordValidator](api:SilverStripe\Security\PasswordValidator):
|
||||
Strong passwords are a crucial part of any system security. So in addition to storing the password in a secure fashion,
|
||||
you can also enforce specific password policies by configuring a
|
||||
[PasswordValidator](api:SilverStripe\Security\PasswordValidator). This can be done through a `_config.php` file
|
||||
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
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\PasswordValidator;
|
||||
```yaml
|
||||
---
|
||||
Name: mypasswords
|
||||
After: '#corepasswords'
|
||||
---
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\Security\PasswordValidator:
|
||||
properties:
|
||||
MinLength: 7
|
||||
HistoricCount: 6
|
||||
MinTestScore: 3
|
||||
|
||||
$validator = new PasswordValidator();
|
||||
$validator->minLength(7);
|
||||
$validator->checkHistoricalPasswords(6);
|
||||
$validator->characterStrength(3, ["lowercase", "uppercase", "digits", "punctuation"]);
|
||||
Member::set_password_validator($validator);
|
||||
# In the case someone uses `new PasswordValidator` instead of Injector, provide some safe defaults through config.
|
||||
SilverStripe\Security\PasswordValidator:
|
||||
min_length: 7
|
||||
historic_count: 6
|
||||
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:
|
||||
|
||||
* `Member.password_expiry_days`: Set the number of days that a password should be valid for.
|
||||
|
@ -26,7 +26,7 @@ To enable the legacy search API on a `GridFieldFilterHeader`, you can either:
|
||||
* set the `useLegacyFilterHeader` property to `true`,
|
||||
* 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
|
||||
SilverStripe\Forms\GridField\GridFieldFilterHeader:
|
||||
force_legacy: true
|
||||
@ -67,4 +67,18 @@ SilverStripe\Core\Injector\Injector:
|
||||
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).
|
||||
|
@ -215,15 +215,6 @@ trait Extensible
|
||||
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.
|
||||
* Note: This will not remove extensions from parent classes, and must be called
|
||||
|
@ -139,9 +139,10 @@ class ConfirmationTokenChain
|
||||
*/
|
||||
public function getRedirectUrlParams()
|
||||
{
|
||||
$params = [];
|
||||
$params = $_GET;
|
||||
unset($params['url']); // CLIRequestBuilder may add this
|
||||
foreach ($this->filteredTokens() as $token) {
|
||||
$params = array_merge($params, $token->getRedirectUrlParams());
|
||||
$params = array_merge($params, $token->params());
|
||||
}
|
||||
|
||||
return $params;
|
||||
|
@ -13,6 +13,16 @@ use SilverStripe\ORM\DataObject;
|
||||
*/
|
||||
class ExtensionTestState implements TestState
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $extensionsToReapply = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $extensionsToRemove = [];
|
||||
|
||||
/**
|
||||
* Called on setup
|
||||
*
|
||||
@ -20,7 +30,6 @@ class ExtensionTestState implements TestState
|
||||
*/
|
||||
public function setUp(SapphireTest $test)
|
||||
{
|
||||
DataObject::flush_extra_methods_cache();
|
||||
}
|
||||
|
||||
public function tearDown(SapphireTest $test)
|
||||
@ -31,6 +40,8 @@ class ExtensionTestState implements TestState
|
||||
{
|
||||
// May be altered by another class
|
||||
$isAltered = false;
|
||||
$this->extensionsToReapply = [];
|
||||
$this->extensionsToRemove = [];
|
||||
|
||||
/** @var string|SapphireTest $class */
|
||||
/** @var string|DataObject $dataClass */
|
||||
@ -46,6 +57,10 @@ class ExtensionTestState implements TestState
|
||||
if (!class_exists($extension) || !$dataClass::has_extension($extension)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($this->extensionsToReapply[$dataClass])) {
|
||||
$this->extensionsToReapply[$dataClass] = [];
|
||||
}
|
||||
$this->extensionsToReapply[$dataClass][] = $extension;
|
||||
$dataClass::remove_extension($extension);
|
||||
$isAltered = true;
|
||||
}
|
||||
@ -62,6 +77,10 @@ class ExtensionTestState implements TestState
|
||||
throw new LogicException("Test {$class} requires extension {$extension} which doesn't exist");
|
||||
}
|
||||
if (!$dataClass::has_extension($extension)) {
|
||||
if (!isset($this->extensionsToRemove[$dataClass])) {
|
||||
$this->extensionsToRemove[$dataClass] = [];
|
||||
}
|
||||
$this->extensionsToRemove[$dataClass][] = $extension;
|
||||
$dataClass::add_extension($extension);
|
||||
$isAltered = true;
|
||||
}
|
||||
@ -85,6 +104,23 @@ class ExtensionTestState implements TestState
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -499,6 +499,17 @@ class TreeDropdownField extends FormField
|
||||
// Begin marking
|
||||
$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
|
||||
$value = $request->requestVar('forceValue') ?: $this->value;
|
||||
if ($value && ($values = preg_split('/,\s*/', $value))) {
|
||||
|
@ -21,13 +21,12 @@ use SilverStripe\Core\Tests\Injector\InjectorTest\NeedsBothCirculars;
|
||||
use SilverStripe\Core\Tests\Injector\InjectorTest\NewRequirementsBackend;
|
||||
use SilverStripe\Core\Tests\Injector\InjectorTest\OriginalRequirementsBackend;
|
||||
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\TestSetterInjections;
|
||||
use SilverStripe\Core\Tests\Injector\InjectorTest\TestStaticInjections;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use stdClass;
|
||||
|
||||
define('TEST_SERVICES', __DIR__ . '/AopProxyServiceTest');
|
||||
|
||||
@ -1048,24 +1047,4 @@ class InjectorTest extends SapphireTest
|
||||
Injector::unnest();
|
||||
$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());
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -167,19 +167,21 @@ class ConfirmationTokenChainTest extends SapphireTest
|
||||
|
||||
public function testGetRedirectUrlParams()
|
||||
{
|
||||
$mockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']);
|
||||
$mockToken = $this->getTokenRequiringReload(true, ['params']);
|
||||
$mockToken->expects($this->once())
|
||||
->method('getRedirectUrlParams')
|
||||
->method('params')
|
||||
->will($this->returnValue(['mockTokenParam' => '1']));
|
||||
|
||||
$secondMockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']);
|
||||
$secondMockToken = $this->getTokenRequiringReload(true, ['params']);
|
||||
$secondMockToken->expects($this->once())
|
||||
->method('getRedirectUrlParams')
|
||||
->method('params')
|
||||
->will($this->returnValue(['secondMockTokenParam' => '2']));
|
||||
|
||||
$chain = new ConfirmationTokenChain();
|
||||
$chain->pushToken($mockToken);
|
||||
$chain->pushToken($secondMockToken);
|
||||
$this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->getRedirectUrlParams());
|
||||
$params = $chain->getRedirectUrlParams();
|
||||
$this->assertEquals('1', $params['mockTokenParam']);
|
||||
$this->assertEquals('2', $params['secondMockTokenParam']);
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ class ErrorControlChainMiddlewareTest extends SapphireTest
|
||||
|
||||
$this->assertInstanceOf(HTTPResponse::class, $result);
|
||||
$location = $result->getHeader('Location');
|
||||
$this->assertContains('?flush=1&flushtoken=', $location);
|
||||
$this->assertContains('flush=1&flushtoken=', $location);
|
||||
$this->assertNotContains('Security/login', $location);
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ class ErrorControlChainMiddlewareTest extends SapphireTest
|
||||
$this->assertInstanceOf(HTTPResponse::class, $result);
|
||||
$location = $result->getHeader('Location');
|
||||
$this->assertContains('/dev/build', $location);
|
||||
$this->assertContains('?devbuildtoken=', $location);
|
||||
$this->assertContains('devbuildtoken=', $location);
|
||||
$this->assertNotContains('Security/login', $location);
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,19 @@ use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\ReadonlyField;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\PasswordValidator;
|
||||
|
||||
class ConfirmedPasswordFieldTest extends SapphireTest
|
||||
{
|
||||
protected $usesDatabase = true;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
PasswordValidator::singleton()->setMinLength(0);
|
||||
}
|
||||
|
||||
public function testSetValue()
|
||||
{
|
||||
$field = new ConfirmedPasswordField('Test', 'Testing', 'valueA');
|
||||
|
@ -9,12 +9,17 @@ use SilverStripe\Dev\CSSContentParser;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Forms\TreeDropdownField;
|
||||
use SilverStripe\ORM\Tests\HierarchyTest\TestObject;
|
||||
|
||||
class TreeDropdownFieldTest extends SapphireTest
|
||||
{
|
||||
|
||||
protected static $fixture_file = 'TreeDropdownFieldTest.yml';
|
||||
|
||||
protected static $extra_dataobjects = [
|
||||
TestObject::class
|
||||
];
|
||||
|
||||
public function testSchemaStateDefaults()
|
||||
{
|
||||
$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()
|
||||
{
|
||||
$field = new TreeDropdownField('TestTree', 'Test tree', Folder::class);
|
||||
|
@ -8,6 +8,7 @@ SilverStripe\Assets\Folder:
|
||||
folder1-subfolder1:
|
||||
Name: FileTest-folder1-subfolder1
|
||||
ParentID: =>SilverStripe\Assets\Folder.folder1
|
||||
|
||||
SilverStripe\Assets\File:
|
||||
asdf:
|
||||
Filename: assets/FileTest.txt
|
||||
@ -24,3 +25,40 @@ SilverStripe\Assets\File:
|
||||
Filename: assets/FileTest-folder1/File1.txt
|
||||
Name: File1.txt
|
||||
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
|
||||
|
@ -22,6 +22,7 @@ use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
|
||||
use SilverStripe\Security\MemberPassword;
|
||||
use SilverStripe\Security\PasswordEncryptor_Blowfish;
|
||||
use SilverStripe\Security\PasswordValidator;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\RememberLoginHash;
|
||||
use SilverStripe\Security\Security;
|
||||
@ -55,7 +56,8 @@ class MemberTest extends FunctionalTest
|
||||
parent::setUp();
|
||||
|
||||
Member::config()->set('unique_identifier_field', 'Email');
|
||||
Member::set_password_validator(null);
|
||||
|
||||
PasswordValidator::singleton()->setMinLength(0);
|
||||
|
||||
i18n::set_locale('en_US');
|
||||
}
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
namespace SilverStripe\Security\Tests;
|
||||
|
||||
use SilverStripe\Security\PasswordValidator;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\PasswordValidator;
|
||||
|
||||
class PasswordValidatorTest extends SapphireTest
|
||||
{
|
||||
@ -14,6 +14,16 @@ class PasswordValidatorTest extends SapphireTest
|
||||
*/
|
||||
protected $usesDatabase = true;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Unset framework default values
|
||||
PasswordValidator::config()
|
||||
->remove('min_length')
|
||||
->remove('historic_count');
|
||||
}
|
||||
|
||||
public function testValidate()
|
||||
{
|
||||
$v = new PasswordValidator();
|
||||
|
Loading…
Reference in New Issue
Block a user