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
|
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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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).
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))) {
|
||||||
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user