diff --git a/_config/passwords.yml b/_config/passwords.yml new file mode 100644 index 000000000..fc865200a --- /dev/null +++ b/_config/passwords.yml @@ -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 diff --git a/docs/en/02_Developer_Guides/00_Model/10_Versioning.md b/docs/en/02_Developer_Guides/00_Model/10_Versioning.md index 40538951d..6ffecad81 100644 --- a/docs/en/02_Developer_Guides/00_Model/10_Versioning.md +++ b/docs/en/02_Developer_Guides/00_Model/10_Versioning.md @@ -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) diff --git a/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md b/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md index cc00ecf0b..70e9a5063 100644 --- a/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md +++ b/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md @@ -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; + } +} +``` + +
+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. +
+ +## Related Lessons + +* [DataExtensions and SiteConfig](https://www.silverstripe.org/learn/lessons/v4/data-extensions-and-siteconfig-1) ## Related Documentaion diff --git a/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md b/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md index 76a03518a..791318d8a 100644 --- a/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md +++ b/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md @@ -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. diff --git a/docs/en/04_Changelogs/4.3.0.md b/docs/en/04_Changelogs/4.3.0.md index 3965ad378..ab31d003e 100644 --- a/docs/en/04_Changelogs/4.3.0.md +++ b/docs/en/04_Changelogs/4.3.0.md @@ -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). diff --git a/src/Core/Extensible.php b/src/Core/Extensible.php index b29dda32e..79fe536b6 100644 --- a/src/Core/Extensible.php +++ b/src/Core/Extensible.php @@ -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 diff --git a/src/Core/Startup/ConfirmationTokenChain.php b/src/Core/Startup/ConfirmationTokenChain.php index a47f2c4c0..e042bb6cb 100644 --- a/src/Core/Startup/ConfirmationTokenChain.php +++ b/src/Core/Startup/ConfirmationTokenChain.php @@ -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; diff --git a/src/Dev/State/ExtensionTestState.php b/src/Dev/State/ExtensionTestState.php index 2cfedddbd..29c4dd09c 100644 --- a/src/Dev/State/ExtensionTestState.php +++ b/src/Dev/State/ExtensionTestState.php @@ -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); + } + } } } diff --git a/src/Forms/TreeDropdownField.php b/src/Forms/TreeDropdownField.php index ca4e11ea9..b5a1cfe84 100644 --- a/src/Forms/TreeDropdownField.php +++ b/src/Forms/TreeDropdownField.php @@ -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))) { diff --git a/tests/php/Core/Injector/InjectorTest.php b/tests/php/Core/Injector/InjectorTest.php index b1dee54db..1e54cc1de 100644 --- a/tests/php/Core/Injector/InjectorTest.php +++ b/tests/php/Core/Injector/InjectorTest.php @@ -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()); - } } diff --git a/tests/php/Core/Injector/InjectorTest/SomeCustomisedExtension.php b/tests/php/Core/Injector/InjectorTest/SomeCustomisedExtension.php deleted file mode 100644 index 9641d7df1..000000000 --- a/tests/php/Core/Injector/InjectorTest/SomeCustomisedExtension.php +++ /dev/null @@ -1,13 +0,0 @@ -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']); } } diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index a90799026..2917c536a 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -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); } diff --git a/tests/php/Forms/ConfirmedPasswordFieldTest.php b/tests/php/Forms/ConfirmedPasswordFieldTest.php index d5b7218d5..3ed909c64 100644 --- a/tests/php/Forms/ConfirmedPasswordFieldTest.php +++ b/tests/php/Forms/ConfirmedPasswordFieldTest.php @@ -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'); diff --git a/tests/php/Forms/TreeDropdownFieldTest.php b/tests/php/Forms/TreeDropdownFieldTest.php index 5b5e22fd1..9af392f4b 100644 --- a/tests/php/Forms/TreeDropdownFieldTest.php +++ b/tests/php/Forms/TreeDropdownFieldTest.php @@ -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); diff --git a/tests/php/Forms/TreeDropdownFieldTest.yml b/tests/php/Forms/TreeDropdownFieldTest.yml index 7546885b3..2207cc300 100644 --- a/tests/php/Forms/TreeDropdownFieldTest.yml +++ b/tests/php/Forms/TreeDropdownFieldTest.yml @@ -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 diff --git a/tests/php/Security/MemberTest.php b/tests/php/Security/MemberTest.php index 4b5341622..e5b7fd303 100644 --- a/tests/php/Security/MemberTest.php +++ b/tests/php/Security/MemberTest.php @@ -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'); } diff --git a/tests/php/Security/PasswordValidatorTest.php b/tests/php/Security/PasswordValidatorTest.php index 4fd99ed08..7fa5637ab 100644 --- a/tests/php/Security/PasswordValidatorTest.php +++ b/tests/php/Security/PasswordValidatorTest.php @@ -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();