Merge branch '4' into pulls/4/deprecating-declared-permissions

This commit is contained in:
Robbie Averill 2019-01-29 09:33:44 +02:00 committed by GitHub
commit b0fc161235
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
247 changed files with 7935 additions and 1146 deletions

View File

@ -1,7 +1,9 @@
## Affected Version
Show version numbers by pasting the output of `composer info --direct`.
Alternatively, hover over the SilverStripe logo in the CMS to basic version information.
Alternatively, get the framework version information from the CMS.
In SilverStripe 4.3 and newer you may find the Help menu in the bottom left corner, unfold it and hover over the version number to get the information.
Otherwise, simply hover over the SilverStripe logo in the bottom left corner of the CMS.
## Description
@ -11,4 +13,4 @@ Please read https://docs.silverstripe.org/en/contributing/issues_and_bugs/
## Steps to Reproduce
Help us with step-by-step instructions.
Help us with step-by-step instructions.

View File

@ -20,20 +20,20 @@ matrix:
include:
- php: 5.6
env:
- DB=MYSQL
- DB=PGSQL
- PHPCS_TEST=1
- PHPUNIT_TEST=framework
- php: 7.0
env:
- DB=PGSQL
- PDO=1
- PHPUNIT_TEST=framework
- php: 7.1
if: type IN (cron)
env:
- DB=MYSQL
- PDO=1
- PHPUNIT_COVERAGE_TEST=framework
- php: 7.2
@ -47,19 +47,10 @@ matrix:
- DB=MYSQL
- PHPUNIT_TEST=cms
- php: 7.3.0RC1
- php: 7.3
env:
- DB=MYSQL
- PDO=1
- PHPUNIT_TEST=framework
sudo: required
dist: xenial
addons:
apt:
packages:
- libzip4
services:
- mysql
before_script:
# Extra $PATH
@ -74,7 +65,7 @@ before_script:
# Install composer dependencies
- composer validate
- mkdir ./public
- if [[ $DB == PGSQL ]]; then composer require silverstripe/postgresql:2.1.x-dev --no-update; fi
- if [[ $DB == PGSQL ]]; then composer require silverstripe/postgresql:2.2.x-dev --no-update; fi
- if [[ $DB == SQLITE ]]; then composer require silverstripe/sqlite3:2.0.x-dev --no-update; fi
- composer require silverstripe/recipe-testing:^1 silverstripe/recipe-core:4.4.x-dev silverstripe/admin:1.4.x-dev silverstripe/versioned:1.4.x-dev --no-update
- if [[ $PHPUNIT_TEST == cms ]]; then composer require silverstripe/recipe-cms:4.4.x-dev --no-update; fi

View File

@ -959,6 +959,9 @@ warnings:
'Object':
message: 'Replaced with traits'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#object-replace'
'SS_Object':
message: 'Replaced with traits'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#object-replace'
'SS_Log':
message: 'Replaced with a PSR-3 logger'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#psr3-logging'
@ -1287,6 +1290,10 @@ warnings:
message: 'Deprecated'
'SilverStripe\Security\Permission::traverse_declared_permissions()':
message: 'Deprecated'
'SilverStripe\Control\Session::get_all()':
message: 'Session can not be accessed statically and `get_all()` is now called `getAll()'
'SilverStripe\Control\Session::clear_all()':
message: 'Session can not be accessed statically and `clear_all()` is now called `clearAll()'
props:
'class':
message: '$this->class access has been removed'
@ -1375,3 +1382,9 @@ warnings:
'THIRDPARTY_DIR':
message: 'Path constants have been deprecated. Use the Requirements and ModuleResourceLoader APIs'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#module-paths'
'SilverStripe\Core\Manifest\ManifestFileFinder::RESOURCES_DIR':
message: 'Use global const RESOURCES_DIR'
url: 'https://docs.silverstripe.org/en/4/changelogs/4.4.0#resources-dir'
replacement: 'RESOURCES_DIR'
renameWarnings:
- Form

View File

@ -19,7 +19,7 @@ and [installation from source](https://doc.silverstripe.org/framework/en/install
## Bugtracker ##
Bugs are tracked on [github.com](https://github.com/silverstripe/silverstripe-framework/issues).
Please read our [issue reporting guidelines](https://doc.silverstripe.org/framework/en/misc/contributing/issues).
Please read our [issue reporting guidelines](https://docs.silverstripe.org/en/4/contributing/issues_and_bugs/).
## Development and Contribution ##

7
_config/buttons.yml Normal file
View File

@ -0,0 +1,7 @@
---
Name: buttons
---
SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
formActions:
showPagination: true
showAdd: true

View File

@ -8,8 +8,16 @@ SilverStripe\Core\Manifest\VersionProvider:
Name: httpconfig-dev
Only:
environment: dev
After:
- 'requestprocessors'
---
# Set dev level to disabled with a higher forcing level
SilverStripe\Control\Middleware\HTTPCacheControlMiddleware:
defaultState: 'disabled'
defaultForcingLevel: 3
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director:
properties:
Middlewares:
ExecMetricMiddleware: '%$SilverStripe\Control\Middleware\ExecMetricMiddleware'

6
_config/gridfield.yml Normal file
View File

@ -0,0 +1,6 @@
---
Name: gridfieldconfig
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Forms\GridField\FormAction\StateStore:
class: SilverStripe\Forms\GridField\FormAction\SessionStore

13
_config/passwords.yml Normal file
View File

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

View File

@ -34,7 +34,7 @@
"psr/container-implementation": "1.0.0",
"silverstripe/config": "^1@dev",
"silverstripe/assets": "^1@dev",
"silverstripe/vendor-plugin": "^1.0",
"silverstripe/vendor-plugin": "^1.4",
"swiftmailer/swiftmailer": "~5.4",
"symfony/cache": "^3.3@dev",
"symfony/config": "^3.2",

View File

@ -48,6 +48,22 @@ use SilverStripe\Core\Environment;
Environment::setEnv('API_KEY', 'AABBCCDDEEFF012345');
```
### Using environment variables in config
To use environment variables in `.yaml` configs you can reference them using backticks.
```yaml
SilverStripe\Core\Injector\Injector:
MyServiceClass:
properties:
MyProperty: '`ENV_VAR_HERE`'
```
<div class="info">
<p>Environment variables cannot be used outside of Injector config as of version 4.2.</p>
</div>
## Including an extra `.env` file
Sometimes it may be useful to include an extra `.env` file - on a shared local development environment where all

View File

@ -11,7 +11,7 @@ Directory | Description
--------- | -----------
`public/` | Webserver public webroot
`public/assets/` | Images and other files uploaded via the SilverStripe CMS. You can also place your own content inside it, and link to it from within the content area of the CMS.
`public/resources/` | Exposed public files added from modules. Folders within this parent will match that of the source root location.
`public/_resources/` | Exposed public files added from modules. Folders within this parent will match that of the source root location (this can be altered by configuration).
`vendor/` | SilverStripe modules and other supporting libraries (the framework is in `vendor/silverstripe/framework`)
`themes/` | Standard theme installation location

View File

@ -67,7 +67,7 @@ check our [community help options](https://www.silverstripe.org/community/).
## Related Lessons
* [Up and running](https://www.silverstripe.org/learn/lessons/v4/up-and-running-setting-up-a-local-silverstripe-dev-environment-1)
* [Creating your first theme](https://www.silverstripe.org/learn/lessons/v4/creating-your-first-theme-1)
* [Creating your first project](https://www.silverstripe.org/learn/lessons/v4/creating-your-first-project)
* [Migrating static templates into your theme](https://www.silverstripe.org/learn/lessons/v4/migrating-static-templates-into-your-theme-1)
* [Working with multiple templates](https://www.silverstripe.org/learn/lessons/v4/working-with-multiple-templates-1)

View File

@ -295,6 +295,15 @@ class Supporter extends DataObject
}
```
To ensure this `many_many` is sorted by "Ranking" by default you can add this to your config:
```yaml
Team_Supporters:
default_sort: '"Team_Supporter"."Ranking" ASC'
```
`Team_Supporters` is the table name automatically generated for the many_many relation in this case.
### many_many through relationship joined on a separate DataObject
If necessary, a third DataObject class can instead be specified as the joining table,
@ -312,6 +321,9 @@ This is declared via array syntax, with the following keys on the many_many:
- `from` Name of the has_one relationship pointing back at the object declaring many_many
- `to` Name of the has_one relationship pointing to the object declaring belongs_many_many.
Just like a any normal DataObject, you can apply a default sort which will be applied when
accessing many many through relations.
Note: The `through` class must not also be the name of any field or relation on the parent
or child record.
@ -348,6 +360,8 @@ class TeamSupporter extends DataObject
'Team' => Team::class,
'Supporter' => Supporter::class,
];
private static $default_sort = '"TeamSupporter"."Ranking" ASC'
}
```
@ -468,6 +482,7 @@ the best way to think about it is that the object where the relationship will be
Product => Categories, the `Product` should contain the `many_many`, because it is much
more likely that the user will select Categories for a Product than vice-versa.
## Cascading deletions
Relationships between objects can cause cascading deletions, if necessary, through configuration of the

View File

@ -91,7 +91,7 @@ class Car extends DataObject
private static $db = [
'Wheels' => 'Int(4)',
'Condition' => 'Enum(array("New","Fair","Junk"), "New")',
'Make' => 'Varchar(["default" => "Honda"]),
'Make' => 'Varchar(["default" => "Honda"])',
);
}
```

View File

@ -291,6 +291,18 @@ $players = Player::get();
$map = $players->map('Name', 'NameWithBirthyear');
```
### Data types
As of SilverStripe 4.4, the following PHP types will be used to return datbase content:
* booleans will be an integer 1 or 0, to ensure consistency with MySQL that doesn't have native booleans.
* integer types returned as integers
* floating point / decimal types returned as floats
* strings returned as strings
* dates / datetimes returned as strings
Up until SilverStripe 4.3, bugs meant that strings were used for every column type.
## Related Lessons
* [Building custom SQL](https://www.silverstripe.org/learn/lessons/v4/beyond-the-orm-building-custom-sql-1)

View File

@ -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)

View File

@ -11,6 +11,69 @@ The examples below are using certain folder naming conventions (CSS files in `cs
SilverStripe core modules like `cms` use a different naming convention (CSS and JavaScript files in `client/src/`).
The `Requirements` class can work with arbitrary file paths.
## Exposing static assets
Before requiring static asset files in PHP code or in a template, those assets need to be "exposed". This process allows SilverStripe projects and SilverStripe modules to make static asset files available via the web server from locations that would otherwise be blocked from web server access, such as the `vendor` folder.
### Configuring your project "exposed" folders
Exposed assets are made available in your web root in a dedicated "resources" directory. Prior to SilverStripe 4.4, the name of this directory was hardcoded to `resources`. In SilverStripe 4.4 and above, the name of the resources directory can be configured by defining the `extra.resources-dir` key in your `composer.json`. SilverStripe projects created from `silverstripe/installer` 4.4 and above will automatically be configured to use `_resources` as their resource directory.
Each folder that needs to be exposed must be entered under the `extra.expose` key in your `composer.json` file. Module developers should use a path relative to the root of their module (don't include the "vendor/package-developer/package-name" path).
This is a sample SilverStripe project `composer.json` file configured to expose some assets.
```json
{
"name": "app/myproject",
"type": "silverstripe-project",
"require": {
"silverstripe/recipe-cms": "4.4.x-dev"
},
"extra": {
"resources-dir": "_resources",
"expose": [
"app/client/dist",
"app/images"
]
}
}
```
Files contained inside the `app/client/dist` and `app/images` will be made publicly available under the `_resources` directory.
SilverStripe projects should not track the "resources" directory in their source control system.
### Exposing assets in the web root
SilverStripe projects ship with `silverstripe/vendor-plugin`. This Composer plugin automatically tries to expose assets from your project and installed modules after installation, or after an update.
Developers can explicitly expose static assets by calling `composer vendor-expose`. This is necessary after updating your `resources-dir` or `expose` configuration in your `composer.json` file.
`composer vendor-expose` accepts an optional `method` argument (e.g.: `composer vendor-expose auto`). This controls how the files are exposed in the "resources" directory:
* `none` disables all symlink / copy
* `copy` copies the exposed files
* `symlink` create symbolic links to the exposed folder
* `junction` uses a junction (Windows only)
* `auto` creates symbolic links (or junctions on Windows), but fails over to copy.
### Referencing exposed assets
When referencing exposed static assets, use either the project file path (relative to the project root folder) or a module name and relative file path to that module's root folder. E.g.:
```php
// When referencing project files, use the same path defined in your `composer.json` file.
Requirements::javascript('app/client/dist/bundle.js');
// When referencing theme files, use a path relative to the root of your project
Requirements::javascript('themes/simple/javascript/script.js');
// When referencing files from a module, you need to prefix the path with the module name.
Requirements::javascript('silverstripe/admin:client/dist/js/bundle.js');
```
When rendered in HTML code, these URLs will be rewritten to their matching path inside the "resources" directory.
## Template Requirements API
**<my-module-dir>/templates/SomeTemplate.ss**
@ -179,7 +242,7 @@ is not appropriate. Normally a single backend is used for all site assets, so a
replaced. For instance, the below will set a new set of dependencies to write to `app/javascript/combined`
```yaml
```yml
---
Name: myrequirements
---

View File

@ -45,7 +45,7 @@ use SilverStripe\View\SSViewer;
public function RenderCustomTemplate()
{
SSViewer::setRewriteHashLinks(false);
$html = $this->renderWith('MyCustomTemplate');
$html = $this->renderWith('My/Namespace/MyCustomTemplate');
SSViewer::setRewriteHashLinks(true);
return $html;

View File

@ -16,12 +16,14 @@ $field = new TextField(..);
$field->setTemplate('MyCustomTextField');
```
Both `MyCustomTemplate.ss` and `MyCustomTextField.ss` should be located in **app/templates/** or the same directory as the core.
To override the template for CMS forms, the custom templates should be located in **/app/templates**. Front-end form templates can be located in **/app/templates** or in the active theme's **/templates** directory.
<div class="notice" markdown="1">
It's recommended to copy the contents of the template you're going to replace and use that as a start. For instance, if
you want to create a `MyCustomFormTemplate` copy the contents of `Form.ss` to a `MyCustomFormTemplate.ss` file and
modify as you need.
*The default Form.ss can be found in `/vendor/silverstripe/framework/templates/SilverStripe/Forms/Includes/`*
</div>
By default, Form and Fields follow the SilverStripe Template convention and are rendered into templates of the same

View File

@ -472,11 +472,29 @@ functionality. See [How to Create a GridFieldComponent](../how_tos/create_a_grid
## Saving the GridField State
`GridState` is a class that is used to contain the current state and actions on the `GridField`. It's transfered
`GridState` is a class that is used to contain the current state and actions on the `GridField`. It's transferred
between page requests by being inserted as a hidden field in the form.
The `GridState_Component` sets and gets data from the `GridState`.
## Saving GridField_FormAction state
By default state used for performing form actions is saved in the session and tagged with a key like `gf_abcd1234`. In
some cases session may not be an appropriate storage method. The storage method can be configured:
```yaml
Name: mysitegridfieldconfig
After: gridfieldconfig
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Forms\GridField\FormAction\StateStore:
class: SilverStripe\Forms\GridField\FormAction\AttributeStore
```
The `AttributeStore` class configures action state to be stored in the DOM and sent back on the request that performs
the action. Custom storage methods can be created and used by implementing the `StateStore` interface and configuring
`Injector` in a similar fashion.
## API Documentation
* [GridField](api:SilverStripe\Forms\GridField\GridField)

View File

@ -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

View File

@ -57,11 +57,11 @@ up by [addons.silverstripe.org](http://addons.silverstripe.org/) website due to
Note that SilverStripe modules have the following distinct characteristics:
- SilverStripe can hook in to the composer installation process by declaring `type: silverstripe/vendormodule`.
- SilverStripe can hook in to the composer installation process by declaring `type: silverstripe-vendormodule`.
- Any folder which should be exposed to the public webroot must be declared in the `extra.expose` config.
These paths will be automatically rewritten to public urls which don't directly serve files from the `vendor`
folder. For instance, `vendor/my-vendor/my-module/client` will be rewritten to
`resources/my-vendor/my-module/client`.
`_resources/my-vendor/my-module/client`.
- Any module which uses the folder expose feature must require `silverstripe/vendor-plugin` in order to
support automatic rewriting and linking. For more information on this plugin you can see the
[silverstripe/vendor-plugin github page](https://github.com/silverstripe/vendor-plugin).

View File

@ -38,6 +38,16 @@ Check the PHPUnit manual for all available [command line arguments](http://www.p
On Linux or OSX, you can avoid typing the full path on every invocation by adding `vendor/bin`
to your `$PATH` definition in the shell profile (usually `~/.profile`): `PATH=./vendor/bin:$PATH`
## Caching
Just like on web requests, SilverStripe caches metadata about the execution context.
This cache can get stale, e.g. when you change YAML configuration or add certain types of PHP code.
In order to flush the cache, use the `flush=1` CLI parameter:
```
vendor/bin/phpunit vendor/silverstripe/framework/tests '' flush=1
```
## Generating a Coverage Report
PHPUnit can generate a code coverage report ([docs](http://www.phpunit.de/manual/current/en/code-coverage-analysis.html))

View File

@ -28,7 +28,8 @@ session variables, used templates and much more.
| isDev | | 1 | | Put the site into [development mode](../), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in. Will persist for the current browser session. |
| isTest | | 1 | | See above. |
| debug | | 1 | | Show a collection of debugging information about the director / controller operation |
| debug_request | | 1 | | Show all steps of the request from initial [HTTPRequest](api:SilverStripe\Control\HTTPRequest) to [Controller](api:SilverStripe\Control\Controller) to Template Rendering |
| debug_request | | 1 | | Show all steps of the request from initial [HTTPRequest](api:SilverStripe\Control\HTTPRequest) to [Controller](api:SilverStripe\Control\Controller) to Template Rendering |
| execmetric | | 1 | | Display the execution time and peak memory usage for the request |
## Classes and Objects

View File

@ -9,5 +9,5 @@ optimization.
SilverStripe does not include any profiling tools out of the box, but we recommend the use of existing tools such as
[XHProf](https://github.com/facebook/xhprof/) and [XDebug](http://xdebug.org/).
* [Profiling with XHProf](http://techportal.inviqa.com/2009/12/01/profiling-with-xhprof/)
* [Profiling PHP Applications With xdebug](http://devzone.zend.com/1139/profiling-php-applications-with-xdebug/)
* [Profiling with XHProf](https://inviqa.com/blog/profiling-xhprof)
* [Profiling PHP Applications With xdebug](http://devzone.zend.com/1139/profiling-php-applications-with-xdebug/)

View File

@ -549,23 +549,50 @@ salt values generated with the strongest entropy generators available on the pla
(see [RandomGenerator](api:SilverStripe\Security\RandomGenerator)). This prevents brute force attacks with
[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.

View File

@ -53,14 +53,15 @@ The simplest way to use [CsvBulkLoader](api:SilverStripe\Dev\CsvBulkLoader) is t
```php
use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Dev\CsvBulkLoader;
class PlayerAdmin extends ModelAdmin
{
private static $managed_models = [
'Player'
Player::class
];
private static $model_importers = [
'Player' => 'CsvBulkLoader',
'Player' => CsvBulkLoader::class,
];
private static $url_segment = 'players';
}

View File

@ -254,16 +254,20 @@ Please ensure that any required plurals are exposed via provideI18nEntities.
```php
// Simple string translation
_t('LeftAndMain.FILESIMAGES','Files & Images');
_t('SilverStripe\\Admin\\LeftAndMain.FILESIMAGES','Files & Images');
// Using injection to add variables into the translated strings.
_t('CMSMain.RESTORED',
_t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
"Restored {value} successfully",
['value' => $itemRestored]
);
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => $count ]);
// You can use __CLASS__ or self::class to reference the current (early bound) class name
_t(self::class . '.GREETING', 'Welcome!');
_t(__CLASS__ . '.GREETING', 'Welcome!');
```
#### Usage in Template Files
@ -410,6 +414,11 @@ Requirements::javascript('silverstripe/admin:client/dist/js/i18n.js');
Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang');
```
You can also include the language files from the public resources folder with the resource syntax:
```php
Requirements::add_i18n_javascript('vendor/module:path/to/lang');
```
### Translation Tables in JavaScript
Translation tables are automatically included as required, depending on the configured locale in `i18n::get_locale()`.

View File

@ -120,6 +120,20 @@ As with storage, there are also different ways of loading the content (or proper
| `File::getAbsoluteURL` | Gets the absolute URL to this resource |
| `File::getMimeType` | Get the mime type of this file |
| `File::getMetaData` | Gets other metadata from the file as an array |
| `File::getFileType` | Return the type of file for the given extension |
### Additional file types
SilverStripe has a pre-defined list of common file types. `File::getFileType` will return "unknown" for files outside that list.
You can add your own file extensions and its description with the following configuration.
```yml
SilverStripe\Assets\File:
file_types:
ai: 'Adobe Illustrator'
psd: 'Adobe Photoshop File'
```
## Modifying files

View File

@ -180,9 +180,9 @@ You can turn this on with the below config:
Name: resamplefiles
---
SilverStripe\Assets\File:
force_resample: false
force_resample: true
SilverStripe\Assets\Storage\DBFile:
force_resample: false
force_resample: true
```
#### Resampled image quality

View File

@ -114,25 +114,37 @@ class Category extends DataObject
{
public function canView($member = null)
{
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member);
}
public function canEdit($member = null)
{
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member);
}
public function canDelete($member = null)
{
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member);
}
public function canCreate($member = null)
{
return Permission::check('CMS_ACCESS_MyAdmin', 'any', $member);
return Permission::check('CMS_ACCESS_Company\Website\MyAdmin', 'any', $member);
}
}
```
## Custom ModelAdmin CSS menu icons using built in icon font
An extended ModelAdmin class supports adding a custom menu icon to the CMS.
```
class NewsAdmin extends ModelAdmin
{
...
private static $menu_icon_class = 'font-icon-news';
}
```
A complete list of supported font icons is available to view in the [SilverStripe Design System Manager](https://projects.invisionapp.com/dsm/silver-stripe/silver-stripe/section/icons/5a8b972d656c91001150f8b6)
## Searching Records

View File

@ -55,15 +55,35 @@ coding conventions.
A pattern library is a collection of user interface design elements, this helps developers and designers collaborate and to provide a quick preview of elements as they were intended without the need to build an entire interface to see it.
Components built in React and used by the CMS are actively being added to the pattern library.
The pattern library can be used to preview React components without including them in the SilverStripe CMS.
To access the pattern library, starting from your project root:
### Viewing the latest pattern library
```
cd vendor/silverstripe/admin && yarn pattern-lib
The easiest way to access the pattern library is to view it online. The pattern library for the latest SilverStripe 4 development branch is automatically built and deployed. Note that this may include new components that are not yet available in a stable release.
[Browse the SilverStripe pattern library online](https://silverstripe.github.io/silverstripe-admin).
### Running the pattern library
If you're developing a new React component, running the pattern library locally is a good way to interact with it.
The pattern library is built from the `silverstripe/admin` module, but it also requires `silverstripe/asset-admin`, `silversrtipe/cms` and `silverstripe/campaign-admin`.
To run the pattern library locally, you'll need a SilverStripe project based on `silverstripe/recipe-cms` and `yarn` installed locally. The pattern library requires the JS source files so you'll need to use the `--prefer-source` flag when installing your dependencies with Composer.
```bash
composer install --prefer-source
(cd vendor/silverstripe/asset-admin && yarn install)
(cd vendor/silverstripe/campaign-admin && yarn install)
(cd vendor/silverstripe/cms && yarn install)
cd vendor/silverstripe/admin && yarn install && yarn pattern-lib
```
Then browse to `http://localhost:6006/`
The pattern library will be available at [http://localhost:6006](http://localhost:6006). The JS source files will be watched, so every time you make a change to a JavaScript file, the pattern library will automatically update itself.
If you want to build a static version of the pattern library, you can replace `yarn pattern-lib` with `yarn build-storybook`. This will output the pattern library files to a `storybook-static` folder.
The SilverStripe pattern library is built using the [StoryBook JS library](https://storybook.js.org/). You can read the StoryBook documentation to learn about more advanced features and customisation options.
## The Admin URL

View File

@ -124,6 +124,27 @@ SilverStripe\Admin\LeftAndMain:
'Feedback': ''
```
## Customising the CMS form actions
The `Previous`, `Next` and `Add` actions on the edit form are visible by default but can be hidden globally by adding the following `.yml` config:
```yml
SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
formActions:
showPagination: false
showAdd: false
```
You can also override this for a specific `GridField` instance when using the `GridFieldConfig_RecordEditor` constructor:
```php
$grid = new GridField(
"pages",
"All Pages",
SiteTree::get(),
GridFieldConfig_RecordEditor::create(null, false, false));
```
## Related
* [How to extend the CMS interface](extend_cms_interface)

View File

@ -89,3 +89,22 @@ __'Scheduled To Publish'__ status. The look of the page node will be changed
from ![Normal Page Node](../../../_images/page_node_normal.png) to ![Scheduled Page Node](../../../_images/page_node_scheduled.png). The getStatusFlags has an `updateStatusFlags()`
extension point, so the flags can be modified through `DataExtension` rather than
inheritance as well. Deleting existing flags works by simply unsetting the array key.
## Customising page icons
The page tree in the CMS is a central element to manage page hierarchies, hence its display of pages can be customised as well. You can specify a custom page icon to make it easier for CMS authors to identify pages of this type, when navigating the tree or adding a new page:
```php
class HomePage extends Page
{
private static $icon_class = 'font-icon-p-home';
}
```
The CMS uses an icon set from [Fontastic](http://fontastic.me/). New icons may be [requested](https://github.com/silverstripe/silverstripe-admin/issues/new) and added to the [core icon set](https://silverstripe.github.io/silverstripe-admin/?selectedKind=Admin%2FIcons&selectedStory=Icon%20reference&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Factions%2Factions-panel). The benefit of having icons added to the core set is that you can use icons more consistently across different modules allowing every module to use a different icon with the same style.
You can also add your own icon by specifying an image path to override the Fontastic icon set:
```php
private static $icon = 'app/images/homepage-icon.svg';
```

View File

@ -24,10 +24,10 @@ the common `Page` object (a new PHP class `MyPage` will look for a `MyPage.ss` t
We can use this to create a different base template with `LeftAndMain.ss`
(which corresponds to the `LeftAndMain` PHP controller class).
Copy the template markup of the base implementation at `templates/SilverStripe/Admin/Includes/LeftAndMain_Menu.ss`
Copy the template markup of the base implementation at `templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss`
from the `silverstripe/admin` module
into `app/templates/Includes/LeftAndMain_Menu.ss`. It will automatically be picked up by
the CMS logic. Add a new section into the `<ul class="cms-menu-list">`
into `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss`. It will automatically be picked up by
the CMS logic. Add a new section into the `<ul class="cms-menu__list">`
```ss
@ -69,26 +69,42 @@ SilverStripe\Admin\LeftAndMain:
- app/css/BookmarkedPages.css
```
In order to let the frontend have the access to our `css` files, we need to `expose` them in the `composer.json`:
```javascript
"extra": {
...
"expose": [
"app/css"
]
},
```
Then run `composer vendor-expose`. This command will publish all the `css` files under the `app/css` folder to their public-facing paths.
> Note: don't forget to `flush`.
## Create a "bookmark" flag on pages
Now we'll define which pages are actually bookmarked, a flag that is stored in
the database. For this we need to decorate the page record with a
`DataExtension`. Create a new file called `app/code/BookmarkedPageExtension.php`
`DataExtension`. Create a new file called `app/src/BookmarkedPageExtension.php`
and insert the following code.
```php
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
class BookmarkedPageExtension extends DataExtension
class BookmarkedPageExtension extends DataExtension
{
private static $db = [
'IsBookmarked' => 'Boolean'
];
public function updateCMSFields(FieldList $fields)
public function updateCMSFields(FieldList $fields)
{
$fields->addFieldToTab('Root.Main',
new CheckboxField('IsBookmarked', "Show in CMS bookmarks?")
@ -117,7 +133,7 @@ pages from the database into the template we've already created (with hardcoded
links)? Again, we extend a core class: The main CMS controller called
`LeftAndMain`.
Add the following code to a new file `app/code/BookmarkedLeftAndMainExtension.php`;
Add the following code to a new file `app/src/BookmarkedLeftAndMainExtension.php`;
```php
@ -143,12 +159,12 @@ SilverStripe\Admin\LeftAndMain:
```
As the last step, replace the hardcoded links with our list from the database.
Find the `<ul>` you created earlier in `app/admin/templates/LeftAndMain.ss`
Find the `<ul>` you created earlier in `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss`
and replace it with the following:
```ss
<ul class="cms-menu-list">
<ul class="cms-menu__list">
<!-- ... -->
<% loop $BookmarkedPages %>
<li class="bookmarked-link $FirstLast">
@ -168,7 +184,7 @@ The following conventions apply:
* New actions can be added by redefining `getCMSActions`, or adding an extension
with `updateCMSActions`.
* It is required the actions are contained in a `FieldSet` (`getCMSActions`
* It is required the actions are contained in a `FieldList` (`getCMSActions`
returns this already).
* Standalone buttons are created by adding a top-level `FormAction` (no such
button is added by default).

View File

@ -143,6 +143,7 @@ upgrade-code all --namespace="App\\Web" --psr4
```
* `--recipe-core-constraint` defines your SilverStripe release version (optional, will default to the most recent stable release).
* `--cwp-constraint` can be used instead `--recipe-core-constraint` when upgrading a CWP project.
* `--namespace` allows you to specify how your project will be namespaced (optional).
* `--psr4` allows you to specify that your project structure respect the PSR-4 standard and to use sub-namespaces.
* `--skip-add-namespace` allows you to skip the `add-namespace` command.
@ -187,7 +188,11 @@ You can upgrade the `composer.json` file with this command:
upgrade-code recompose --write
```
You can add a `--recipe-core-constraint` flag to target a specific version of `silverstripe/recipe-core`. By default, the project will be upgraded to the latest stable version. You can use the `--strict` option if you want to use more conservative version constraints. Omit the `--write` flag to preview your changes.
You can add a `--recipe-core-constraint` flag to target a specific version of `silverstripe/recipe-core`. By default, the project will be upgraded to the latest stable version. If you are upgrading a CWP project, you can use `--cwp-constraint` instead to target a specific version of `cwp/cwp-core`.
The upgrader uses [carret version constraint](https://getcomposer.org/doc/articles/versions.md#caret-version-range-) by default. This will cause composer to install compatible minor releases. You can use the `--strict` option if you want to use the more conservative [tilde version constraints](https://getcomposer.org/doc/articles/versions.md#tilde-version-range-).
Omit the `--write` flag to preview your changes.
Your upgraded `composer.json` file will look like this.
```json
@ -598,17 +603,36 @@ Execute the upgrade command with this command.
upgrade-code upgrade ./mysite/ --write
```
If you omit the `--write` flag you will get a preview of what change the upgrader will apply to your codebase. This can be helpful if you if you are tweaking your `.upgrade.yml` or if you are trying to identify areas where you should add a `@skipUpgrade` statement,
If you omit the `--write` flag you will get a preview of what change the upgrader will apply to your codebase. This can be helpful if you are tweaking your `.upgrade.yml` or if you are trying to identify areas where you should add a `@skipUpgrade` statement,
You can also tweak which rules to apply with the `--rule` flag: `code`, `config`, and `lang`. For example, the following command will only upgrade `lang` and `config` files:
```bash
upgrade-code upgrade ./mysite/ --rule=config --rule=lang
```
The `upgrade` command can alter big chunks of your codebase. While it works reasonably well in most use case, you should not trust it blindly. You should take time to review all changes applied by the `upgrade` command and confirm you are happy with them.
The `upgrade` command can alter big chunks of your codebase. While it works reasonably well in most use cases, you should not trust it blindly. You should take time to review all changes applied by the `upgrade` command and confirm you are happy with them.
[Continue to "Finalising namespace updates"](#namespace-finalise)
#### Rename Warnings
You can also show extra warnings for potentially ambiguous mappings with the `renameWarnings` property:
```yaml
renameWarnings:
- File
- Image
```
An example of an ambiguous rename would be:
```PHP
private static $has_one = [
'Image' => 'Image',
];
```
Add the `--prompt` flag to manually approve ambiguous class renames.
### Manually update namespaced references
If you decide to update your namespace references by hand, you'll need to go through the entire code base and update them all from the old non-namespaced SilverStripe classes to the new namespaced equivalent. If you are referencing classes from third party modules that have been namespaced, you'll need to update those as well.
@ -1022,14 +1046,14 @@ If you are using a modified `index.php`, `.htaccess`, or `web.config`, you will
* `assets`
* Any `favicon` files
* Other common files that should be accssible in your project webroot (example: `robots.txt`)
* Delete the root `resources` directory if present.
* Delete the root `resources` or `_resources` directories if present.
* Run the following command `composer vendor-expose` to make static assets files accessible via the `public` directory.
If you are upgrading from SilverStripe 4.0 to SilverStripe 4.1 (or above), you'll need to update `index.php` before moving it to the public folder. You can get a copy of the generic `index.php` file from `vendor/silverstripe/recipe-core/public`. If you've made modifications to your `index.php` file, you'll need to replicate those into the new `public/index.php` file.
### Finalising the web root migration
You'll need to update your server configuration to point to the public directory rather than the root of your project.
Update your `.gitignore` file so `assets` and `resources` are still ignored when located under the `public` folder.
Update your `.gitignore` file so `assets` and `_resources` (or `resources` if using a pre SilverStripe 4.4 release) are still ignored when located under the `public` folder.
Your project should still be functional, although you may now be missing some static assets.
This is a good point to commit your changes to your source control system before moving on to the next step.
@ -1126,7 +1150,22 @@ All your assets should be loading properly now.
This is a good point to commit your changes to your source control system before moving on to the next step.
## Step 10 - Running your upgraded site for the first time {#step10}
## Step 10 - Update database class references {#step10}
If you've updated your class names to use namespaces you will need to reflect those changes in any existing database fields. For example, if you've renamed your `HomePage` class to `App\HomePage` then the database `ClassName` column needs to be updated to point to the `App\HomePage` class, otherwise the CMS will tell you that the page is obsolete. This also applies to polymorphic relationships.
There is no automated way to do this, but you can use the list generated in .upgrade.yml and copy it to `app/_config/legacy.yml`, removing any classes that don't extend DataObject.
```
SilverStripe\ORM\DatabaseAdmin:
classname_value_remapping:
HomePage: App\HomePage
```
This will automatically update affected columns when you first build the database.
## Step 11 - Running your upgraded site for the first time {#step11}
You're almost across the finish line.

View File

@ -746,13 +746,13 @@ Because the filesystem now uses the sha1 of file contents in order to version mu
filename, the default storage paths in 4.0 will not be the same as in 3.
In order to retain existing file paths in line with framework version 3 you should set the
`\SilverStripe\Filesystem\Flysystem\FlysystemAssetStore.legacy_paths` config to true.
`\SilverStripe\Filesystem\Flysystem\FlysystemAssetStore.legacy_filenames` config to true.
Note that this will not allow you to utilise certain file versioning features in 4.0.
```yml
SilverStripe\Filesystem\Flysystem\FlysystemAssetStore:
legacy_paths: true
legacy_filenames: true
```

View File

@ -10,3 +10,103 @@ behaviour to that of SilverStripe 3 where `Extension` instances are of lowest im
default value. If you rely on your `Extension` or module providing an overriding config value, please move this to yaml.
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-11-02 [aebaa46](https://github.com/silverstripe/silverstripe-admin/commit/aebaa46f1f8834fefa09bdaf85bfdd51229f58b3) Add CSRF to Apollo (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-08-21 [8d7c2dafa](https://github.com/silverstripe/silverstripe-framework/commit/8d7c2dafabad505d769f3774c44e0595fb1a4cd9) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019)
* 2018-07-29 [9aabe0a0f](https://github.com/silverstripe/silverstripe-framework/commit/9aabe0a0f7a061d87cc92923f8811e14d7a032f5) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018)
* 2018-05-08 [19fdebfa2](https://github.com/silverstripe/silverstripe-framework/commit/19fdebfa245506626561bc9626d9ac325acb14da) Remove dotm, potm, jar, css, js, xltm from default File.allowed_extensions (Robbie Averill) - See [ss-2018-014](https://www.silverstripe.org/download/security-releases/ss-2018-014)
* 2018-04-11 [577138882](https://github.com/silverstripe/silverstripe-framework/commit/577138882163e4b8782ea043487944d30d88e753) Restrict non-admins from being assigned to admin groups (Damian Mooyman) - See [ss-2018-001](https://www.silverstripe.org/download/security-releases/ss-2018-001)
### API Changes
* 2017-09-12 [c54b07a95](https://github.com/silverstripe/silverstripe-framework/commit/c54b07a9528aeef3907b4342a725af10d9797cd8) Update to use new chromedriver + behat-extension + facebook/webdriver (Damian Mooyman)
### Features and Enhancements
* 2018-04-19 [1509a12fd](https://github.com/silverstripe/silverstripe-framework/commit/1509a12fdf0fe8cbd300271fd5c60c3d76647d84) Only run coverage test as a cron (Damian Mooyman)
* 2018-04-09 [87d69ba7](https://github.com/silverstripe/silverstripe-cms/commit/87d69ba75366ff63563e5b9b159fb643daa4f1d7) Use i18n template for page tree title (Damian Mooyman)
* 2018-03-05 [32637413d](https://github.com/silverstripe/silverstripe-framework/commit/32637413deceb1a3c647fd51a78e1352e91ee15a) Improve upgrade rules to support advanced upgrader rewrites (#7903) (Damian Mooyman)
* 2018-03-05 [8c35e339](https://github.com/silverstripe/silverstripe-cms/commit/8c35e3391cf334917c5e314c6f1c459e9d06fbbc) Improve upgrade rules to support advanced upgrader rewrites (#2114) (Damian Mooyman)
* 2018-03-01 [61cfcc5](https://github.com/silverstripe/silverstripe-versioned/commit/61cfcc52a46895b6ff7fdb12956e01c48bfcf343) Improve upgrade rules to support advanced upgrader rewrites (Damian Mooyman)
* 2018-03-01 [e77e0f7](https://github.com/silverstripe/silverstripe-assets/commit/e77e0f758ed243a249c6056dbfc44f48b9fa535d) Improve upgrade rules to support advanced upgrader rewrites (Damian Mooyman)
* 2018-02-12 [9ce21338a](https://github.com/silverstripe/silverstripe-framework/commit/9ce21338a3083c80128c5923eab3d8b968f4dd83) composer.json missing notice (zanderwar)
* 2017-11-15 [c7ab5846d](https://github.com/silverstripe/silverstripe-framework/commit/c7ab5846df7e3f460b1c38e04a0946a914a35c19) Don't infer trace if explicitly provided (Damian Mooyman)
### Bugfixes
* 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville)
* 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) fix description for docs.silverstripe.org (wernerkrauss)
* 2018-09-03 [b922c0d73](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill)
* 2018-08-28 [d651d0fbf](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill)
* 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater)
* 2018-06-13 [a2a8004](https://github.com/silverstripe/silverstripe-admin/commit/a2a800464b8f430529ee291a8b75e422ceca7914) Update user help link to 4 (Sacha Judd)
* 2018-06-13 [932eb2b2](https://github.com/silverstripe/silverstripe-cms/commit/932eb2b22dfe6c30473b1cf973661c28c5b9c635) Fix CMS components failing to register on other CMS sections (#2182) (Damian Mooyman)
* 2018-06-12 [7b04949ca](https://github.com/silverstripe/silverstripe-framework/commit/7b04949caa11d6e5c8cace3453cf2ed29996fb06) Remove duplicate key (Damian Mooyman)
* 2018-06-12 [c9bcc07](https://github.com/silverstripe/silverstripe-assets/commit/c9bcc070fdbb76fef49f7564eb98a4a81e2ed65f) Remove duplicate .upgrade.yml keys (Damian Mooyman)
* 2018-06-12 [674b92c](https://github.com/silverstripe/silverstripe-admin/commit/674b92c125488cb6bc43cade4c93e9adccb27e9b) Fix invalid .upgrade.yml (Damian Mooyman)
* 2018-06-11 [2a51f34c3](https://github.com/silverstripe/silverstripe-framework/commit/2a51f34c3e3c44acd603def241ac4447e715b165) Prevent canonical URL causing a redirect on CLI unless explicitly enabled (Damian Mooyman)
* 2018-06-07 [29f9b1c18](https://github.com/silverstripe/silverstripe-framework/commit/29f9b1c18fb38dab912a0b9dcae63eacae19335d) Fix linting issues (Damian Mooyman)
* 2018-06-07 [e37e3e174](https://github.com/silverstripe/silverstripe-framework/commit/e37e3e1746e56c866ee875f41a7fddf61c926d9f) Fix test that relies on implicit ID order breaking postgres (Damian Mooyman)
* 2018-06-07 [66f57bd4d](https://github.com/silverstripe/silverstripe-framework/commit/66f57bd4dac0bd4c8106f8071ddc45103c2643f2) Only set MYSQL_ATTR_INIT_COMMAND when using mysql driver (fixes #8103) (Loz Calver)
* 2018-06-06 [c070e989c](https://github.com/silverstripe/silverstripe-framework/commit/c070e989c4de41441d1061d2678b461f3f13d63b) Safely handle empty injector factory responses (Damian Mooyman)
* 2018-06-04 [41e601a03](https://github.com/silverstripe/silverstripe-framework/commit/41e601a036307065d9ea2ba8862f67be738d402f) Regression from #8009 (Daniel Hensby)
* 2018-06-01 [5a5ba1e5c](https://github.com/silverstripe/silverstripe-framework/commit/5a5ba1e5c001de161fbeb19d6d662391dccc4c1e) Fix: negative values in read only currency field (Jonathon Menz)
* 2018-06-01 [582c69d32](https://github.com/silverstripe/silverstripe-framework/commit/582c69d32fd8f18e6c06bc0b4c0a7e3e87e67966) Fix issue with Disabled DateField always display (not set). (Maxime Rainville)
* 2018-05-29 [1cbf27e0f](https://github.com/silverstripe/silverstripe-framework/commit/1cbf27e0f47c3547914b03193d0f5f77c87ff8d5) PHP 5.3 compat for referencing $this in closure, and make method public for same reason (Robbie Averill)
* 2018-05-21 [bf5b578](https://github.com/silverstripe/silverstripe-admin/commit/bf5b5787685765c35c175c303f3f7ee719ac9453) Adding a min-width to flexbox-area-grow that allows flex blocks to shrink below their content width (Guy)
* 2018-05-18 [953153500](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill)
* 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver)
* 2018-05-02 [80bf0fc48](https://github.com/silverstripe/silverstripe-framework/commit/80bf0fc48774b2a25f95feb24ffcc9df8e5ad77c) bad syntax (Daniel Hensby)
* 2018-04-18 [fe4b90edc](https://github.com/silverstripe/silverstripe-framework/commit/fe4b90edc0ead9c6c77d606101bfbf568a963fb4) Duplicating many_many relationships looses the extra fields in 4.0 (UndefinedOffset)
* 2018-04-17 [f83691e7f](https://github.com/silverstripe/silverstripe-framework/commit/f83691e7f7e7a75657df1211673b72d9cf4c4b4f) Make invalid dev actions 404 not 500 error (Damian Mooyman)
* 2018-04-17 [af3a9f3ec](https://github.com/silverstripe/silverstripe-framework/commit/af3a9f3ec8a5465f841c5aa8ee1faf40c1b76bf4) Duplicating many_many relationships looses the extra fields (fixes #7973) (UndefinedOffset)
* 2018-04-10 [e11ba9a2d](https://github.com/silverstripe/silverstripe-framework/commit/e11ba9a2d7c89a1ecea8613589f05399b45a33bf) Fix many_many through crashing ModelAdmin (Damian Mooyman)
* 2018-04-08 [eeac1d1](https://github.com/silverstripe/silverstripe-admin/commit/eeac1d11800e70f19055bfa2ba4aec8b6a9b2ccb) Fix issue with selected values in large trees breaking initialisation (#476) (Damian Mooyman)
* 2018-03-29 [4acec3356](https://github.com/silverstripe/silverstripe-framework/commit/4acec33562e4e1230092eee7d76c2b8061ffc914) Fixed bug in config merging priorities so that config values set by extensions are now least important instead of most important (Daniel Hensby)
* 2018-03-28 [dd44deacb](https://github.com/silverstripe/silverstripe-framework/commit/dd44deacb462d80dbbda507fdb4e9527f049d3bd) Fix for "too few parameters" error when using DBMultiEnum (Andreas Lindahl)
* 2018-03-22 [cf5a0984](https://github.com/silverstripe/silverstripe-cms/commit/cf5a0984addf308d2cb10df9b67386be2a080f18) Correct SilverStripeNavigator correctly in templates (Daniel Hensby)
* 2018-03-15 [d17d93f7](https://github.com/silverstripe/silverstripe-cms/commit/d17d93f784a6e01f3d396c55adc623d69a90261a) Remove SearchForm results() function from allowed_actions (Steve Dixon)
* 2018-03-11 [2b9faf46](https://github.com/silverstripe/silverstripe-cms/commit/2b9faf46fe6606a9236f9e1ec987f9a22689a2c7) Fix InSection failing on non-page controllers (Damian Mooyman)
* 2018-03-07 [bf2cee398](https://github.com/silverstripe/silverstripe-framework/commit/bf2cee3989028aaa461e9f0f929724b7738c1399) Bugfix - Correct duplicate nesting of 'Content' to be returned to template (Joe Harvey)
* 2018-03-06 [5fee4a81a](https://github.com/silverstripe/silverstripe-framework/commit/5fee4a81aa880338fba7bb72731fd2b7be4643de) Files dataobjects with missing asset shouldn't un-attach themselves from parent object on save (Damian Mooyman)
* 2018-03-05 [dde13493](https://github.com/silverstripe/silverstripe-cms/commit/dde134936825e196ca97cb86ac3f5bc24d52278e) Fix invalid css classname in virtualpage (Damian Mooyman)
* 2018-03-05 [985a0af](https://github.com/silverstripe/silverstripe-admin/commit/985a0af292bb833ba48fa907a1b75892182ec390) Fix page icons (Damian Mooyman)
* 2018-03-02 [3bd714d](https://github.com/silverstripe/silverstripe-assets/commit/3bd714d293c3f6c12e8f7a0b3c7e054d99b410bd) Typo in "audio file" translation (Robbie Averill)
* 2018-03-01 [40c2e299a](https://github.com/silverstripe/silverstripe-framework/commit/40c2e299a0a9a63b4e64e14dff95e9f7d480db6e) Fix "mb_stripos(): Empty delimiter" warning when no search-keywords are given for `DBText::ContextSummary`. (Roman Schmid)
* 2018-02-27 [d91c6ed](https://github.com/silverstripe/silverstripe-admin/commit/d91c6ed0dc699f769802e5d1310f3fe111dd8ecf) Fix $CMSVersion appearing visually (Damian Mooyman)
* 2018-02-26 [b27102f81](https://github.com/silverstripe/silverstripe-framework/commit/b27102f810e873d287fa04678a4ff242c40699f6) Fix incorrect assets created when ASSETS_PATH !== BASE_PATH . '/assets' (Damian Mooyman)
* 2018-02-22 [012bfec5](https://github.com/silverstripe/silverstripe-cms/commit/012bfec5bf8e0902f3325c8e7fb237d48bd189ad) Bug field help text translations no longer need to be HTML encoded (Rick Hambrook)
* 2018-02-20 [83c4ab8d](https://github.com/silverstripe/silverstripe-cms/commit/83c4ab8d180954b3d80d16ed5f5764e3c647ca6d) Fix test regressions in CMS page filters (Damian Mooyman)
* 2018-02-19 [cfe82e9](https://github.com/silverstripe/silverstripe-assets/commit/cfe82e912616ca230b8fd29fba3bd3270fac2502) Fix behaviour towards versioned but unstagable records (Damian Mooyman)
* 2018-02-19 [4fc8166](https://github.com/silverstripe/silverstripe-versioned/commit/4fc816653e84c0a883a01afe30e16e8bd4129f53) Fix behaviour towards versioned but unstagable records (Damian Mooyman)
* 2018-02-19 [0e26c0664](https://github.com/silverstripe/silverstripe-framework/commit/0e26c066440d2591401e84d9688cbeef0595afcc) Fix behaviour towards versioned but unstagable records (Damian Mooyman)
* 2018-02-19 [3be0478e](https://github.com/silverstripe/silverstripe-cms/commit/3be0478e1c40cd2b9f577818596c4222b365b6b6) Fix behaviour towards versioned but unstagable records (Damian Mooyman)
* 2018-02-19 [8be3930](https://github.com/silverstripe/silverstripe-versioned/commit/8be393061e8578c3bd9056c6540e5f0bbff43801) Fix doRollbackTo() writing old / unsaved version over restored version (Damian Mooyman)
* 2018-02-16 [86addea1d](https://github.com/silverstripe/silverstripe-framework/commit/86addea1d2a7b2e28ae8115279ae358bcb46648a) Split HTML manipulation to onadd, so elements are not accidentally duplicated (Christopher Joe)
* 2018-02-13 [c767e472d](https://github.com/silverstripe/silverstripe-framework/commit/c767e472dc494408460ef47c27b8d34475da4ac6) DataObject singleton creation (Jonathon Menz)
* 2018-02-13 [f2b82b1f7](https://github.com/silverstripe/silverstripe-framework/commit/f2b82b1f77a60de4bf1b5807e1b820aad263ae1b) Fix docs for configuring before/after a specific config file (Christopher Joe)
* 2018-02-13 [c6095cf](https://github.com/silverstripe/silverstripe-config/commit/c6095cfc0a07a74bb932e2191215d06f102e992a) Fix word boundary issue with pathname matching (Christopher Joe)
* 2018-02-13 [1d27a14](https://github.com/silverstripe/silverstripe-admin/commit/1d27a14be75efb33a503f7f1c15b093ab3b59c7f) Remove border-radius add hover states to non-active tabs (Sacha Judd)
* 2018-02-12 [ad52ced](https://github.com/silverstripe/silverstripe-versioned/commit/ad52ced4353b8abe312aeacfb2c95657169feedc) Prevent nested permissions from breaking recursive publishing (Damian Mooyman)
* 2018-02-12 [0f08f85](https://github.com/silverstripe/silverstripe-admin/commit/0f08f85508d01a578015848caff032ae0fd62e4c) improve the browser warning logic show (Christopher Joe)
* 2018-02-08 [d86e5dfc](https://github.com/silverstripe/silverstripe-cms/commit/d86e5dfc883267ffaa0c43e9ece7576c4f42ed61) remove now superfluous print action destroyer (Dylan Wagstaff)
* 2018-02-08 [d3278d547](https://github.com/silverstripe/silverstripe-framework/commit/d3278d5470165bba14ee5026453ec7d529901f42) Add Nested DB transaction support (#7848) (Daniel Hensby)
* 2018-02-08 [0a486b8f5](https://github.com/silverstripe/silverstripe-framework/commit/0a486b8f5705242de523489190f3975d55b3b3e6) Fix issue with CLIDebugView failing on class name of existing class (Damian Mooyman)
* 2018-02-06 [0094c19](https://github.com/silverstripe/silverstripe-admin/commit/0094c19304eea5ac02daf42095da341315dae84f) Add text-colour to status-archived, remove span.badge styles (Sacha Judd)
* 2018-02-06 [6b38031a1](https://github.com/silverstripe/silverstripe-framework/commit/6b38031a1e16e94d5bafcbcce4bdcb2d6b3680ed) Fix Director::test() not persisting removed session keys on teardown (Damian Mooyman)
* 2018-02-06 [660dfd34a](https://github.com/silverstripe/silverstripe-framework/commit/660dfd34a828e7eb7dc8ef9986b201a14620d17f) Issue where default admin has no password encryption (Daniel Hensby)
* 2018-02-05 [28ca11dd7](https://github.com/silverstripe/silverstripe-framework/commit/28ca11dd7e5e9a1c4fd1f5d4acbec856adfb7176) Regex range identifier correctly escaped (Daniel Hensby)
* 2018-02-04 [1ff32b3](https://github.com/silverstripe/silverstripe-admin/commit/1ff32b347911c1a6f8521f31e79131db68ed3084) Ensure lang is detected from html tag (Martin P)
* 2018-01-26 [416915b08](https://github.com/silverstripe/silverstripe-framework/commit/416915b08248285083518850ad8d015ca8ed25c2) tableName is blank in CompositeDBField-&gt;addToQuery (Dominik Beerbohm)
* 2018-01-25 [cf69d0486](https://github.com/silverstripe/silverstripe-framework/commit/cf69d048665befa90eb43146f86cde984b876b3a) Fix ping including requirements (Damian Mooyman)
* 2018-01-24 [c2cd6b383](https://github.com/silverstripe/silverstripe-framework/commit/c2cd6b3832c6bc4775b2742df593b445c2aca391) Fix Member_GroupSet::removeAll() (fixes #3948) (Loz Calver)
* 2018-01-24 [f2b4c192e](https://github.com/silverstripe/silverstripe-framework/commit/f2b4c192ec4d70779f7c667a976e741a7f3a26c5) Fix UploadField cuts off “Save” button (closes #2862) (Loz Calver)
* 2018-01-23 [7384e3fc2](https://github.com/silverstripe/silverstripe-framework/commit/7384e3fc25987742ea08af74b704857a936e8ec0) Gridfields with dropdowns having lots of overflow (Scott Hutchinson)
* 2018-01-19 [5849820](https://github.com/silverstripe/silverstripe-asset-admin/commit/58498200190cba086477c158d1fe6112cf3b0a1e) Fix compatibility issue with chromedriver (Damian Mooyman)
* 2016-10-21 [8e5bb6fbd](https://github.com/silverstripe/silverstripe-framework/commit/8e5bb6fbdce0b2ca2d08a45534df2264db5e6b12) Fix : relObject() should return null if one of the node is null (Jason)
* 2016-03-15 [22b3a71ec](https://github.com/silverstripe/silverstripe-framework/commit/22b3a71ec0c8cd8c38030fa0bf5449abefafe8a3) fixing val reference to url in https hotlink (Denise Rivera)
* 2015-04-22 [1f63637b9](https://github.com/silverstripe/silverstripe-framework/commit/1f63637b9369d4644a92523ada5d1a5dc0576c12) for #4095, TinyMCE not able to modify props of embed media (bug 1) and invalid HTML inserted (bug 2) (Patrick Nelson)

View File

@ -0,0 +1,19 @@
# 4.0.6
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-12-10 [0e841aa](https://github.com/silverstripe/silverstripe-graphql/commit/0e841aabb7372d9fa78108e4819e151608ddec0f) Apply CSRF middlware API
* 2018-11-07 [48bd33564](https://github.com/silverstripe/silverstripe-framework/commit/48bd335648188df9dae72be1e5f9c808f3fe1e77) Ensure that table names are escaped to prevent possible SQL injection (Robbie Averill) - See [ss-2018-020](https://www.silverstripe.org/download/security-releases/ss-2018-020)
### Bugfixes
* 2018-11-29 [59221e8](https://github.com/silverstripe/silverstripe-assets/commit/59221e8d74ac5e07b86a741e2709e0676130f7b4) Cache key cannot contain : chars, will happen when viewing from archive (Robbie Averill)
* 2018-11-21 [9ce6d91b7](https://github.com/silverstripe/silverstripe-framework/commit/9ce6d91b76e525a6fc81e02023e9e53cdf82e047) / TreeMultiselectField::objectForKey handles list of IDs correctly (Serge Latyntcev)
* 2018-11-16 [35c3a8c6](https://github.com/silverstripe/silverstripe-cms/commit/35c3a8c68db2660838dcd2ae5abd2bd1c3214af4) 'Search' text in default search form should be a placeholder (Robbie Averill)
* 2018-11-15 [b5bae137b](https://github.com/silverstripe/silverstripe-framework/commit/b5bae137bd341eeda3f4886f45fc8f8d657a9c4c) Redirect loop with multiple confirmation tokens present (fixes #8607) (Loz Calver)
* 2018-11-12 [15aaf9db9](https://github.com/silverstripe/silverstripe-framework/commit/15aaf9db9fe1679cf8b01b74fce3eee841278495) Fix a code style typo (Serge Latyntcev)
* 2018-11-08 [4b4fbabed](https://github.com/silverstripe/silverstripe-framework/commit/4b4fbabed5d70bf577e4b0d6fdbc9dab9da80451) TreeMultiselectField passes value 'unchanged' as null to ORM for 'ID' column key (Serge Latyntcev)

View File

@ -10,3 +10,39 @@ behaviour to that of SilverStripe 3 where `Extension` instances are of lowest im
default value. If you rely on your `Extension` or module providing an overriding config value, please move this to yaml.
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-11-06 [6cb1bf5](https://github.com/silverstripe/silverstripe-admin/commit/6cb1bf53a6fd5b54b6f7bbe7a1d7b939e176cf53) Add CSRF protection (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-08-21 [af000be](https://github.com/silverstripe/silverstripe-framework/commit/af000bea9b16ea553cae7f7f662f74ab8dc343df) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019)
* 2018-07-29 [5425195](https://github.com/silverstripe/silverstripe-framework/commit/54251952387394d72b221e797a80edfbf9a973ee) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018)
### Features and Enhancements
* 2018-04-18 [fef734b](https://github.com/silverstripe/recipe-core/commit/fef734b5484d86f5afd4e857c556b8c1d8d66c16) Provide default IIS rewriting rules with recipe (Damian Mooyman)
### Bugfixes
* 2018-10-24 [e72fc9e](https://github.com/silverstripe/silverstripe-framework/commit/e72fc9e3d0f35a1d43f55f83f9919f67d72fb7cb) DataObject singleton creation (#8516) (Sam Minnée)
* 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville)
* 2018-09-13 [5c102de](https://github.com/silverstripe/silverstripe-cms/commit/5c102decbde43395e14aeff83a20c4c6f1d048ae) Improve performance of CMSMain::getArchiveWarningMessage (#2231) (Maxime Rainville)
* 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) description for docs.silverstripe.org (wernerkrauss)
* 2018-09-03 [b922c0d](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill)
* 2018-08-28 [d651d0f](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill)
* 2018-08-27 [4da5569](https://github.com/silverstripe/silverstripe-framework/commit/4da5569232505ee574e0b5106ff2116611393aa4) ensure createFromVariables takes correct params on CLIRequestBuilder (Scott Hutchinson)
* 2018-08-15 [0c713b5](https://github.com/silverstripe/silverstripe-assets/commit/0c713b5b1eb6a08ac00dcadb187b8b3ef7115fc4) Fix routing for files with dots in filename (Damian Mooyman)
* 2018-08-14 [27ac001](https://github.com/silverstripe/silverstripe-framework/commit/27ac001d5b27cce4f80ce4b3335c14708b116830) email rendering should not include requirements (Thomas Portelange)
* 2018-08-14 [8ec551e](https://github.com/silverstripe/silverstripe-cms/commit/8ec551e57b04d00d6897d06c2779557f0ec8109d) Broken "show as list" (#2232) (Maxime Rainville)
* 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater)
* 2018-07-14 [a0e0bed](https://github.com/silverstripe/recipe-core/commit/a0e0bed7e7fe83b98264563efdeffa82d0d01d04) Use Injector to create PasswordValidators (Daniel Hensby)
* 2018-07-14 [8703839](https://github.com/silverstripe/silverstripe-framework/commit/8703839eb142ba0414f4d84f885ff898c39d6786) updateValidatePassword calls need to be masked from backtraces (Daniel Hensby)
* 2018-07-12 [e80c7e7](https://github.com/silverstripe/silverstripe-cms/commit/e80c7e712b916712d4ec7b6b8359ccf71dc9da04) Restore button now has warning colour and correct icon (Robbie Averill)
* 2018-07-12 [d122995](https://github.com/silverstripe/silverstripe-framework/commit/d1229956523d69f63c9e725b261c0142d5ee1de3) Duplicate config values for cascade_duplicates no longer duplicate their duplicates (Robbie Averill)
* 2018-06-19 [725212a](https://github.com/silverstripe/silverstripe-framework/commit/725212a707f6b724aff6548c3680b2cd66e9a6bb) Allow dispatcher in Embed to be configured with injector (#8192) (Robbie Averill)
* 2018-06-13 [932eb2b](https://github.com/silverstripe/silverstripe-cms/commit/932eb2b22dfe6c30473b1cf973661c28c5b9c635) Fix CMS components failing to register on other CMS sections (#2182) (Damian Mooyman)
* 2018-05-21 [bf5b578](https://github.com/silverstripe/silverstripe-admin/commit/bf5b5787685765c35c175c303f3f7ee719ac9453) Adding a min-width to flexbox-area-grow that allows flex blocks to shrink below their content width (Guy)
* 2018-05-18 [9531535](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill)
* 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver)
* 2018-03-29 [4acec33](https://github.com/silverstripe/silverstripe-framework/commit/4acec33562e4e1230092eee7d76c2b8061ffc914) Fixed bug in config merging priorities so that config values set by extensions are now least important instead of most important (Daniel Hensby)

View File

@ -0,0 +1,20 @@
# 4.1.4
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-12-10 [6f8dc77](https://github.com/silverstripe/silverstripe-graphql/commit/6f8dc779f39aebf79acbc0e2f3363705833b583b) Apply CSRF middlware API
* 2018-11-07 [fecedc2d9](https://github.com/silverstripe/silverstripe-framework/commit/fecedc2d98eeaaff6424fb59dc70ef6bdc6dc92d) Ensure that table names are escaped to prevent possible SQL injection (Robbie Averill) - See [ss-2018-020](https://www.silverstripe.org/download/security-releases/ss-2018-020)
### Bugfixes
* 2018-11-29 [59221e8](https://github.com/silverstripe/silverstripe-assets/commit/59221e8d74ac5e07b86a741e2709e0676130f7b4) Cache key cannot contain : chars, will happen when viewing from archive (Robbie Averill)
* 2018-11-21 [9ce6d91b7](https://github.com/silverstripe/silverstripe-framework/commit/9ce6d91b76e525a6fc81e02023e9e53cdf82e047) / TreeMultiselectField::objectForKey handles list of IDs correctly (Serge Latyntcev)
* 2018-11-16 [35c3a8c6](https://github.com/silverstripe/silverstripe-cms/commit/35c3a8c68db2660838dcd2ae5abd2bd1c3214af4) 'Search' text in default search form should be a placeholder (Robbie Averill)
* 2018-11-15 [b5bae137b](https://github.com/silverstripe/silverstripe-framework/commit/b5bae137bd341eeda3f4886f45fc8f8d657a9c4c) Redirect loop with multiple confirmation tokens present (fixes #8607) (Loz Calver)
* 2018-11-12 [15aaf9db9](https://github.com/silverstripe/silverstripe-framework/commit/15aaf9db9fe1679cf8b01b74fce3eee841278495) Fix a code style typo (Serge Latyntcev)
* 2018-11-08 [4b4fbabed](https://github.com/silverstripe/silverstripe-framework/commit/4b4fbabed5d70bf577e4b0d6fdbc9dab9da80451) TreeMultiselectField passes value 'unchanged' as null to ORM for 'ID' column key (Serge Latyntcev)
* 2018-10-15 [6de0fa0](https://github.com/silverstripe/silverstripe-versioned/commit/6de0fa087fe581b69a5978db82058490c44923b4) Fix codesniffer runs in Travis (Robbie Averill)

View File

@ -0,0 +1,61 @@
# 4.2.2
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-08-21 [0610f76da](https://github.com/silverstripe/silverstripe-framework/commit/0610f76da02ac53a1b51cdfe9eac34e943a66991) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019)
* 2018-08-12 [909ab03](https://github.com/silverstripe/silverstripe-admin/commit/909ab03fc4e742a05a06c33c5233691fd7466836) Add CSRF to Apollo (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-07-29 [214e28127](https://github.com/silverstripe/silverstripe-framework/commit/214e28127f5425b61c15b69f884afdbad31133c2) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018)
### Features and Enhancements
* 2018-08-24 [2b335b4](https://github.com/silverstripe/silverstripe-graphql/commit/2b335b4239946f9a6fb1d525452cf1fe6d22a9ce) Proof of concept of cached graphql queries (#166) (Damian Mooyman)
* 2018-07-01 [73d3da2](https://github.com/silverstripe/silverstripe-admin/commit/73d3da2bc8566cb1cb5da0124b7deb513728b5ab) Pattern library now has FormAction examples (Robbie Averill)
### Bugfixes
* 2018-10-24 [e72fc9e3d](https://github.com/silverstripe/silverstripe-framework/commit/e72fc9e3d0f35a1d43f55f83f9919f67d72fb7cb) DataObject singleton creation (#8516) (Sam Minnée)
* 2018-10-19 [7c65916](https://github.com/silverstripe/silverstripe-asset-admin/commit/7c659167f2eda63d882a097f2f413b9f3cb79e31) Use fixtured file title in test assertion (Robbie Averill)
* 2018-10-17 [d71ee0c](https://github.com/silverstripe/silverstripe-admin/commit/d71ee0ce9898e73c9a7d913356fc6bfe6c2b42fc) Fixes #674 TinyMCE width - this should match form field widths at lower width resolutions but expand up to the max width on wider resolutions (bergice)
* 2018-10-16 [a6a174399](https://github.com/silverstripe/silverstripe-framework/commit/a6a17439976710b2311558d363b5467fa429dcca) Fix `ENTER` not triggering form save button as `GridField`s used `submit` type buttons (bergice)
* 2018-10-14 [c0c446a](https://github.com/silverstripe/silverstripe-versioned/commit/c0c446ad8f29dd66398feb38f5d92fa4f60a4a8b) Fix relations between staged/unstaged objects (Harsh Chokshi)
* 2018-10-09 [f710c5c](https://github.com/silverstripe/silverstripe-admin/commit/f710c5cdcd2cf95fdaa738f55c0f2529fcbe826d) Only hide overflow from inactive chosen fields (Robbie Averill)
* 2018-10-01 [5422e28](https://github.com/silverstripe/silverstripe-asset-admin/commit/5422e28635cec8f285eb422fa85f57f4418c09b8) Folder sort incorrect (Luke Edwards)
* 2018-09-28 [231d6d9a9](https://github.com/silverstripe/silverstripe-framework/commit/231d6d9a9f388e10cf77149aec22e947db648644) New members now receive the configured default locale, not the current locale (Robbie Averill)
* 2018-09-21 [1d5ecd342](https://github.com/silverstripe/silverstripe-framework/commit/1d5ecd342e417b4707a3bbc34e97949bffd14afb) Prevent error on valid response status codes (Damian Mooyman)
* 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville)
* 2018-09-18 [db63f55fb](https://github.com/silverstripe/silverstripe-framework/commit/db63f55fbb8e635e4e7215b7b7eff4e1f1cb7b22) Changes being detected on TreeMulti as values not sorted (Luke Edwards)
* 2018-09-13 [5c102dec](https://github.com/silverstripe/silverstripe-cms/commit/5c102decbde43395e14aeff83a20c4c6f1d048ae) Improve performance of CMSMain::getArchiveWarningMessage (#2231) (Maxime Rainville)
* 2018-09-10 [8ae0ef0](https://github.com/silverstripe/silverstripe-versioned/commit/8ae0ef0002a229d233f7395cfed15c979c3f1698) Do not update LeftAndMain link with Stage param (#173) (Maxime Rainville)
* 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) fix description for docs.silverstripe.org (wernerkrauss)
* 2018-09-03 [b922c0d73](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill)
* 2018-08-31 [68c2c976d](https://github.com/silverstripe/silverstripe-framework/commit/68c2c976d4813607a420ac4cda7b01f0a7aee8c7) Fix alignment test step definition (#8354) (Luke Edwards)
* 2018-08-30 [234b795f8](https://github.com/silverstripe/silverstripe-framework/commit/234b795f89657c6b25da6101a9fc878e3297c301) Use classes for TinyMCE alignment buttons (Luke Edwards)
* 2018-08-28 [d651d0fbf](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill)
* 2018-08-27 [4da556923](https://github.com/silverstripe/silverstripe-framework/commit/4da5569232505ee574e0b5106ff2116611393aa4) ensure createFromVariables takes correct params on CLIRequestBuilder (Scott Hutchinson)
* 2018-08-27 [f3230c78](https://github.com/silverstripe/silverstripe-reports/commit/f3230c78d4e3731a10a5f4c508bc68c6a8534866) Use requestVar() to include post vars as well as get vars (Robbie Averill)
* 2018-08-23 [f37dd74](https://github.com/silverstripe/silverstripe-admin/commit/f37dd74be7afae5e40e85ce2a90a4d92bf7e80bb) Site tree items do not disappear on save with source file comments enabled (Robbie Averill)
* 2018-08-20 [dbab69669](https://github.com/silverstripe/silverstripe-framework/commit/dbab6966908f0a293ee6d469cec6b4650dc5a0f1) Message when changing password with invalid token now contains correct links to login (Robbie Averill)
* 2018-08-20 [9da7f99](https://github.com/silverstripe/silverstripe-versioned/commit/9da7f991f33ac16070b2e47b764b216a87f96622) Draft content requiring login message now correctly renders HTML link (Robbie Averill)
* 2018-08-17 [c361b09](https://github.com/silverstripe/silverstripe-admin/commit/c361b091b1640c25f1d23914489212fce1e29377) overflow of chosen dropdowns when inactive (Scott Hutchinson)
* 2018-08-16 [66cd3af](https://github.com/silverstripe/silverstripe-admin/commit/66cd3af09fcf68bf177a46ac57434442642d1b7c) Filtering or paginating a gridfield causing a change event (Luke Edwards)
* 2018-08-15 [0db594b2d](https://github.com/silverstripe/silverstripe-framework/commit/0db594b2d39c93dd2e911414bee5520c84048906) Remove double escaping of HTML values in print views (Robbie Averill)
* 2018-08-15 [0c713b5](https://github.com/silverstripe/silverstripe-assets/commit/0c713b5b1eb6a08ac00dcadb187b8b3ef7115fc4) Fix routing for files with dots in filename (Damian Mooyman)
* 2018-08-14 [873873dc3](https://github.com/silverstripe/silverstripe-framework/commit/873873dc303ce2041aa23e365464133a359e1561) Pass request to dummy controller before calling init (Robbie Averill)
* 2018-08-14 [27ac001d5](https://github.com/silverstripe/silverstripe-framework/commit/27ac001d5b27cce4f80ce4b3335c14708b116830) email rendering should not include requirements (Thomas Portelange)
* 2018-08-14 [8ec551e5](https://github.com/silverstripe/silverstripe-cms/commit/8ec551e57b04d00d6897d06c2779557f0ec8109d) Broken "show as list" (#2232) (Maxime Rainville)
* 2018-08-12 [9f5b0086c](https://github.com/silverstripe/silverstripe-framework/commit/9f5b0086cb1a0259c5c87ea205390c5e69dcae90) Paginating a gridfield causing a change event (Luke Edwards)
* 2018-08-10 [d4995f52](https://github.com/silverstripe/silverstripe-cms/commit/d4995f5204f020f75fbddb3e49b944a54be5c6c2) Separating ModelAsController catch-all route to apply after all other configuration (Guy Marriott)
* 2018-08-08 [e14ab99](https://github.com/silverstripe/silverstripe-graphql/commit/e14ab991f5c99cee6b1bdfa18ab07a1e4b40961e) Don't rely on return value of GraphQL scaffolding providers (#171) (Guy Marriott)
* 2018-08-06 [df7396e8](https://github.com/silverstripe/silverstripe-cms/commit/df7396e8845eea7a75e73237de9ee7e4cb6568f6) CMS routes are now run after #coreroutes without re-including itself (Robbie Averill)
* 2018-07-27 [85b4b48fb](https://github.com/silverstripe/silverstripe-framework/commit/85b4b48fb5489cdba4b18cbf510d883986dd61c1) Restore default delete action on GridFieldConfig_RecordEditor (Maxime Rainville)
* 2018-07-27 [0d90cdb05](https://github.com/silverstripe/silverstripe-framework/commit/0d90cdb05d058763e5e52720ab653c5cc391dc3b) Altering ID of authenticator tabs to resolve ID conflict (Guy Marriott)
* 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater)
* 2018-07-23 [a0487e5](https://github.com/silverstripe/silverstripe-admin/commit/a0487e59fc04af0d15e66d4c2874051288b4e63e) Treat readonly as disabled and fix handling for ui-constructive class (Robbie Averill)
* 2018-07-16 [e1296d48](https://github.com/silverstripe/silverstripe-reports/commit/e1296d4813ac1b677aa7a612ba0ad3b2ba62ccae) Filter var can be returned correctly from get variables as a fallback (Robbie Averill)
* 2018-06-27 [8ccebf8](https://github.com/silverstripe/silverstripe-admin/commit/8ccebf813e95980363a92ec37332d2241327441f) Stop sslink from hijacking anchor plugin (Will Rossiter)
* 2018-05-18 [953153500](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill)
* 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver)

View File

@ -0,0 +1,28 @@
# 4.2.3
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-12-10 [b59ba39](https://github.com/silverstripe/silverstripe-graphql/commit/b59ba397ff42d8934bd2d9c932514f898c327f64) Add CSRF middlware
## Features and Enhancements
* 2018-07-16 [9270206c](https://github.com/silverstripe/silverstripe-reports/commit/9270206c3bd2fe35bb263ad43ad3a5d87360873a) Use Injector to create new class instances and pass $params (Robbie Averill)
### Bugfixes
* 2018-12-10 [9fce4b2](https://github.com/silverstripe/silverstripe-graphql/commit/9fce4b2408dd82d303925eee3b6cf393da371e85) Ensure httpMethod context is applied to all controller actions (#194) (Aaron Carlino)
* 2018-11-29 [59221e8](https://github.com/silverstripe/silverstripe-assets/commit/59221e8d74ac5e07b86a741e2709e0676130f7b4) Cache key cannot contain : chars, will happen when viewing from archive (Robbie Averill)
* 2018-11-21 [9ce6d91b7](https://github.com/silverstripe/silverstripe-framework/commit/9ce6d91b76e525a6fc81e02023e9e53cdf82e047) / TreeMultiselectField::objectForKey handles list of IDs correctly (Serge Latyntcev)
* 2018-11-16 [35c3a8c6](https://github.com/silverstripe/silverstripe-cms/commit/35c3a8c68db2660838dcd2ae5abd2bd1c3214af4) 'Search' text in default search form should be a placeholder (Robbie Averill)
* 2018-11-15 [b5bae137b](https://github.com/silverstripe/silverstripe-framework/commit/b5bae137bd341eeda3f4886f45fc8f8d657a9c4c) Redirect loop with multiple confirmation tokens present (fixes #8607) (Loz Calver)
* 2018-11-12 [15aaf9db9](https://github.com/silverstripe/silverstripe-framework/commit/15aaf9db9fe1679cf8b01b74fce3eee841278495) Fix a code style typo (Serge Latyntcev)
* 2018-11-08 [4b4fbabed](https://github.com/silverstripe/silverstripe-framework/commit/4b4fbabed5d70bf577e4b0d6fdbc9dab9da80451) TreeMultiselectField passes value 'unchanged' as null to ORM for 'ID' column key (Serge Latyntcev)
* 2018-10-15 [6de0fa0](https://github.com/silverstripe/silverstripe-versioned/commit/6de0fa087fe581b69a5978db82058490c44923b4) Fix codesniffer runs in Travis (Robbie Averill)
* 2018-10-06 [c498aa03](https://github.com/silverstripe/silverstripe-cms/commit/c498aa03379ca883803dda853e64c411ed7454dc) Fixing wrong Live-Preview-Link in SilverStripeNavigatorItem_LiveLink (fixes #865). (Stephan Bauer)
* 2018-09-13 [7189653b](https://github.com/silverstripe/silverstripe-cms/commit/7189653b1f9a744b9ee2393a8ef3fb8597c89b1b) SiteTree Title field should have rounded corners before Update URL button is shown (Robbie Averill)
* 2018-07-27 [bc70b877](https://github.com/silverstripe/silverstripe-reports/commit/bc70b87721c8278111e39e0af69db1052af7333f) Apply missing class to report header. (Maxime Rainville)
* 2018-07-01 [bc8bb13](https://github.com/silverstripe/silverstripe-campaign-admin/commit/bc8bb13c93c75e718872315a60f0eb8213bd8e69) Button outline secondary class is now correct in disabled "Publish campaign" button (Robbie Averill)

View File

@ -7,6 +7,8 @@
- Take care with `stageChildren()` overrides. `Hierarchy::numChildren() ` results will only make use of `stageChildren()` customisations that are applied to the base class and don't include record-specific behaviour.
- New React-based search UI for the CMS, Asset-Admin, GridFields and ModelAdmins.
- A new `GridFieldLazyLoader` component can be added to `GridField`. This will delay the fetching of data until the user access the container Tab of the GridField.
- `SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController` is now the default CMS history controller and `SilverStripe\CMS\Controllers\CMSPageHistoryController` has been deprecated.
- PHPUnit tests no longer auto-flush, requiring manual flush parameters when changing YAML config or certain PHP code
## Upgrading {#upgrading}
@ -25,6 +27,12 @@ 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):
```yml
SilverStripe\Forms\GridField\GridFieldFilterHeader:
force_legacy: true
```
```php
public function getCMSFields()
{
@ -41,3 +49,263 @@ public function getCMSFields()
}
```
### Keep using the legacy `CMSPageHistoryController`
To keep using the old CMS history controller for every page type, add the following entry to your YML config.
```yml
SilverStripe\Core\Injector\Injector:
SilverStripe\CMS\Controllers\CMSPageHistoryController:
class: SilverStripe\CMS\Controllers\CMSPageHistoryController
```
If you want to use both CMS history controllers in different contexts, you can implement your own _Factory_ class.
```yml
SilverStripe\Core\Injector\Injector:
SilverStripe\CMS\Controllers\CMSPageHistoryController:
factory:
App\MySite\MyCustomControllerFactory
```
[Implementing a _Factory_ with the Injector](/developer_guides/extending/injector/#factories)
### PHPUnit tests no longer auto-flush
SilverStripe caches certain metadata in manifests, for example YAML configuration
and certain PHP class structures (e.g. `ClassInfo::implementorsOf()`).
This is also the case for CLI executions such as PHPUnit.
Starting with SilverStripe 4.0, PHPUnit executions flushed the cache automatically.
While this meant less work for developers in manually flushing caches,
it significantly increased the time to run each test (from sub-second to multi-second).
In order to allow for efficient test execution and Test Driven Development (TDD),
we have decided to treat this as a performance regression.
In order to flush manifests on test execution, please use the following command
(note the empty quoted string):
```
vendor/bin/phpunit vendor/silverstripe/framework/tests '' flush=1
```
See our [testing guide](https://docs.silverstripe.org/en/4/developer_guides/testing/)
for more details.
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-12-11 [4f68e4b](https://github.com/silverstripe/silverstripe-admin/commit/4f68e4b759c104b2690050af034a41d370d2e6e4) Add CSRF middleware implementation (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-11-07 [e18549beb](https://github.com/silverstripe/silverstripe-framework/commit/e18549beb08b47f63c87df3f1d727313e380bb95) Ensure that table names are escaped to prevent possible SQL injection (Robbie Averill) - See [ss-2018-020](https://www.silverstripe.org/download/security-releases/ss-2018-020)
* 2018-10-24 [88391f2](https://github.com/silverstripe/silverstripe-graphql/commit/88391f27463cac553360d7d94b0760e797247855) CSRF protection (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-08-21 [3dbb10625](https://github.com/silverstripe/silverstripe-framework/commit/3dbb10625c6918094a18cd9a29f1f9daca8129c5) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019)
* 2018-08-12 [3424431](https://github.com/silverstripe/silverstripe-admin/commit/3424431a5ed52fc9aae97423268aea5eba7334a2) Add CSRF to Apollo (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-07-29 [637b4225c](https://github.com/silverstripe/silverstripe-framework/commit/637b4225c6a85bfa0d59e516a8c602203cc980d9) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018)
### API Changes
* 2018-10-05 [5276b6cbb](https://github.com/silverstripe/silverstripe-framework/commit/5276b6cbb1ebd49d704734d940b0467c51b0d064) Add FieldList::getContainerField (Maxime Rainville)
* 2018-10-04 [dba237e](https://github.com/silverstripe/silverstripe-admin/commit/dba237e5d6ab13a7a5f8cbc9a9e594d98478c566) Allow Gridfield to be lazy loadable. (Maxime Rainville)
* 2018-10-04 [bdb53979a](https://github.com/silverstripe/silverstripe-framework/commit/bdb53979aaed743f278b299e3dab7b13e321fbe7) Create a new GridFieldLazyLoader GridField component (Maxime Rainville)
* 2018-09-28 [cb2b9498f](https://github.com/silverstripe/silverstripe-framework/commit/cb2b9498fb33bd57a19ba6a7ceed6d3fbc976d70) Deprecate updateSearchContextCallback and updateSearchFormCallback (Robbie Averill)
* 2018-09-03 [f48ab77](https://github.com/silverstripe/silverstripe-assets/commit/f48ab777c2845d7b414ebae0ff7903c6bb423340) Add `show_file_link_tracking` config to `FileLinkTracking` extension to control visibility of the File Tracking tab (bergice)
* 2018-08-31 [01db5c9e9](https://github.com/silverstripe/silverstripe-framework/commit/01db5c9e98f5406e8d6b0f1f4ea660edb24b199f) Add Link Tracking section to Relations developer guide and describe `show_sitetree_link_tracking`, `show_file_link_tracking`. (bergice)
* 2018-08-31 [115ed92e](https://github.com/silverstripe/silverstripe-cms/commit/115ed92e0aed63df1620700955afd6248fa97fae) Add `show_sitetree_link_tracking` config to `SiteTreeLinkTracking` extension to control visibility of the Link Tracking tab (bergice)
* 2018-08-05 [02be4cc](https://github.com/silverstripe/silverstripe-graphql/commit/02be4cc3e1cab7c1ed8e8f9d6dde6ee102364fa6) Multi-schema support (#169) (Aaron Carlino)
* 2018-07-12 [0343203](https://github.com/silverstripe/silverstripe-versioned/commit/03432030e99a496c6cd81f4bec780164851aa955) Add new RestoreAction and canRestoreToDraft method (#168) (Luke Edwards)
### Features and Enhancements
* 2018-11-01 [2f2fb2b](https://github.com/silverstripe/silverstripe-admin/commit/2f2fb2bfdcd88b54ac9426f172bec77a3c210172) Improve loading screen and indicator (#582) (Luke Edwards)
* 2018-10-29 [e51be58](https://github.com/silverstripe/silverstripe-asset-admin/commit/e51be58685527b512bf20e7542d506aef14f4ed8) Move Remove button into preview-image message (#859) (Sacha Judd)
* 2018-10-29 [2440432](https://github.com/silverstripe/silverstripe-asset-admin/commit/24404324790a26b0c19f68f862bb2679618199ee) Move the replace file into the more options action set (#848) (Sacha Judd)
* 2018-10-29 [952f37a](https://github.com/silverstripe/silverstripe-admin/commit/952f37abdc78d9f87da20461537c88cf570d7cba) Adding an HOC to provide DragDropContext for consumers of ReactDND (#711) (Guy Marriott)
* 2018-10-26 [2c3665d](https://github.com/silverstripe/silverstripe-admin/commit/2c3665d83375364249adf02c2eef6382d73e7c43) Adding a component for a generic popover filled with buttons (#684) (Guy Marriott)
* 2018-10-19 [e88b8e7](https://github.com/silverstripe/silverstripe-admin/commit/e88b8e7b2fa4c47e62d737ea3a3ccee8c7ff8794) Expose TabsActions (#703) (Raissa North)
* 2018-10-18 [761a6f7](https://github.com/silverstripe/silverstripe-admin/commit/761a6f7f6f809c36f21dd804ce1d6325909b5d1c) Reverse argument signature of methods using path (#698) (Aaron Carlino)
* 2018-10-17 [902fec0](https://github.com/silverstripe/silverstripe-asset-admin/commit/902fec0f89bb49783b2dc5f64b4d0c24c33531fb) Extensible readFiles query (#847) (Aaron Carlino)
* 2018-10-17 [437e53f2f](https://github.com/silverstripe/silverstripe-framework/commit/437e53f2fec17bc778cc7bba39de322d43214441) Some minor refactoring of the PDO and MySQLi connectors (Robbie Averill)
* 2018-10-15 [3e2ce31](https://github.com/silverstripe/silverstripe-admin/commit/3e2ce3138d88674fc27aa465dc9be9ea4533c830) Add nested fields, args, distribute args to fields (#683) (Aaron Carlino)
* 2018-10-08 [2775895d0](https://github.com/silverstripe/silverstripe-framework/commit/2775895d03af93d207079637a8eb6afdfad5ab01) Adding a helper to find a form field by label content (Guy Marriott)
* 2018-10-05 [6e66c48](https://github.com/silverstripe/silverstripe-admin/commit/6e66c48d65b52d568fbd34f8a7c630b4a829c2c9) Add CMS community help menu to cms-menu (#615) (Sacha Judd)
* 2018-10-04 [9ea7b58a8](https://github.com/silverstripe/silverstripe-framework/commit/9ea7b58a8f26ca8856211da30eed5751706d0c4b) Add memory cache to ThemeResourceLoader::findTemplate() (Robbie Averill)
* 2018-10-03 [90afb2037](https://github.com/silverstripe/silverstripe-framework/commit/90afb2037a6d2d9be3f605aad9ddfbda0bbab2e7) TabSet react component is no longer structural (Raissa North)
* 2018-10-03 [0be4a9a](https://github.com/silverstripe/silverstripe-admin/commit/0be4a9abfa4482deb2753c50c47dbf0192581a12) Adding an extension point to FormBuilderLoader after redux form is initialised (Raissa North)
* 2018-10-02 [cba5d30](https://github.com/silverstripe/silverstripe-admin/commit/cba5d307f499471b88f77fd201df2a4baacf0038) Connect Tabs component to redux-form to handle activeTab state (Raissa North)
* 2018-10-02 [e1f2f89d3](https://github.com/silverstripe/silverstripe-framework/commit/e1f2f89d37e44c706d350d02774733dd867ccdc7) Add test for PHP 7.3 support (SS4 version) (Sam Minnee)
* 2018-09-28 [bd37b90a](https://github.com/silverstripe/silverstripe-cms/commit/bd37b90a3a5811555248ef616698efbe24466b11) Add CMSMain.enable_archive_warning_message config (Sam Minnee)
* 2018-09-27 [4b155b9](https://github.com/silverstripe/silverstripe-graphql/commit/4b155b982ca6fd737aa70e281ce679bb43404060) Add getSortableFields to return sortable fields for query (#185) (Robbie Averill)
* 2018-09-27 [25759ffc5](https://github.com/silverstripe/silverstripe-framework/commit/25759ffc5fe532a239b1487ca6b025140d2e144f) Show file path on PHP parser exceptions (Ingo Schommer)
* 2018-09-25 [5b7a84141](https://github.com/silverstripe/silverstripe-framework/commit/5b7a84141b0fbef66a9f3f52a9ccee12e02ef1e0) Add Hierarchy::prepopulate_numchildren_cache() (#8380) (Sam Minnée)
* 2018-09-21 [e2701a4](https://github.com/silverstripe/silverstripe-admin/commit/e2701a4cd00ffc2136b935038d8422a25acfa2dd) TinyMCE inline toolbar for images and embeds (Luke Edwards)
* 2018-09-20 [c928e83](https://github.com/silverstripe/silverstripe-versioned/commit/c928e830825ec8ae5336d254835f754a11bac350) Backport of DataList for list of versions, for SS 4.3.x (Robbie Averill)
* 2018-09-19 [d9a6c10](https://github.com/silverstripe/silverstripe-admin/commit/d9a6c10a157d6c1fd17351b0097ce9d8c5336ecf) Form state & schema persists across form remounting (Guy Marriott)
* 2018-09-19 [40dde226f](https://github.com/silverstripe/silverstripe-framework/commit/40dde226fd1b8997308ef0f5718763a298295cdf) Add ?showqueries=backtrace (Sam Minnee)
* 2018-09-19 [6de12e1](https://github.com/silverstripe/silverstripe-asset-admin/commit/6de12e1375d146d61052f1a6dd001aa3a6c437bc) TinyMCE inline toolbar for images and embeds (Luke Edwards)
* 2018-09-17 [588bf83e1](https://github.com/silverstripe/silverstripe-framework/commit/588bf83e1238a79793da4bc4145b7597ae2626be) Add hideNav flag to schema defaults (Raissa North)
* 2018-09-17 [a800ce7](https://github.com/silverstripe/silverstripe-admin/commit/a800ce714a99a631b8a7401322df0653e2ee476c) Add hideNav flag to allow hiding of navigation in Tabs (Raissa North)
* 2018-09-12 [d8bf873](https://github.com/silverstripe/silverstripe-admin/commit/d8bf873e1236ff5cb634cce0a47c992366ae6139) Use bootstrap modal footer, use our icon font for close icon (Luke Edwards)
* 2018-09-12 [c03c685](https://github.com/silverstripe/silverstripe-asset-admin/commit/c03c685da247b8baa6d4180816bda35bc29fcf2d) Use bootstrap modal footer, use our icon font for close icon (Luke Edwards)
* 2018-09-03 [2394194](https://github.com/silverstripe/silverstripe-admin/commit/239419424ee001200830bc8b77dfef3ce7a05d7e) HtmlEditorField component for react rich text (Dylan Wagstaff)
* 2018-08-22 [a257c1c](https://github.com/silverstripe/silverstripe-admin/commit/a257c1cea84ebcdd27df5043c3690bdcb09f43da) Add toggleCallback function (Raissa North)
* 2018-08-22 [8153d12](https://github.com/silverstripe/silverstripe-admin/commit/8153d12537b53bd208ddb28675aa82bd84da2a16) Add dropdownToggleClassName prop to ActionMenu (Raissa North)
* 2018-08-20 [26262ea](https://github.com/silverstripe/silverstripe-admin/commit/26262ea3a9d64e4172ed277845a53e58c48772ef) Insert link shortcut for HTMLEditorField (#599) (Luke Edwards)
* 2018-08-12 [8d3b022](https://github.com/silverstripe/silverstripe-assets/commit/8d3b02259167ce08fd30742475a29219d64df0c8) Support setting quality on a per-image basis (#153) (Loz Calver)
* 2018-08-05 [9d0ae97](https://github.com/silverstripe/silverstripe-admin/commit/9d0ae970e5cdf2ec8710b471884d75b5485b9309) ViewModeToggle states are now stored in constants (Robbie Averill)
* 2018-08-01 [4213eeb](https://github.com/silverstripe/silverstripe-asset-admin/commit/4213eeb1513662ef2dd177065310c2062532a424) Use the new general purpose search component. (#812) (Maxime Rainville)
* 2018-08-01 [5a00d84](https://github.com/silverstripe/silverstripe-admin/commit/5a00d8462ec2806c640f1214971f4747f276d398) General purpose search form component (#572) (Maxime Rainville)
* 2018-07-30 [163ca65](https://github.com/silverstripe/silverstripe-graphql/commit/163ca65a1f2e858c7d47d4b15775153ea0451d5b) DataObjectScaffolder instantiation is now handled through Injector (Robbie Averill)
* 2018-07-30 [24d3023](https://github.com/silverstripe/silverstripe-graphql/commit/24d3023b0d361fee66b4e68aca1814b7dfdbcac2) Allow non-internal input types passed as args (#168) (Aaron Carlino)
* 2018-07-25 [79a5ea3](https://github.com/silverstripe/recipe-cms/commit/79a5ea3dace336558ef4d5be4a86cc1a0e84badc) Add versioned-admin (Luke Edwards)
* 2018-07-17 [337da78](https://github.com/silverstripe/silverstripe-campaign-admin/commit/337da782bd520beebdb734542b5403b69257d058) Update webpack-config constraint (Raissa North)
* 2018-07-16 [786446fb](https://github.com/silverstripe/silverstripe-reports/commit/786446fb670905832b6bfe49775b9c2eaff262cc) Use Injector to create new class instances and pass $params (Robbie Averill)
* 2018-07-12 [f2ebdb7f](https://github.com/silverstripe/silverstripe-cms/commit/f2ebdb7f5ec894e8c0c8f29b9aac984aef4ca8ed) add SiteTree::updateAnchorsOnPage() for user defining additional page anchors (Will Rossiter)
* 2018-07-12 [114b0a5ea](https://github.com/silverstripe/silverstripe-framework/commit/114b0a5ea7ea6b8f33b8c9b8d1611e5ee6619a1c) Option for secure "remember me" cookie (Ingo Schommer)
* 2018-07-12 [3292a8b77](https://github.com/silverstripe/silverstripe-framework/commit/3292a8b773c5b29a69b72718f996a36f3daead1d) Add `columnUnique` API SS_List classes. (Al Twohill)
* 2018-07-06 [6c1a34c](https://github.com/silverstripe/silverstripe-campaign-admin/commit/6c1a34cc2c9bfbc3bca471c7ca77976bac194918) Make use of ViewModeToggle component. (Raissa North)
* 2018-04-15 [5108734](https://github.com/silverstripe/silverstripe-admin/commit/5108734b19edbe9d6404b747e25059a586b009fe) Add ViewModeToggle component (Raissa North)
* 2018-01-30 [1857f00](https://github.com/silverstripe/silverstripe-admin/commit/1857f00bd2b172acb933064d3d454e91bc855200) Add tests for Form component (Robbie Averill)
* 2018-01-30 [cc945f0](https://github.com/silverstripe/silverstripe-admin/commit/cc945f09aac38dea25ec57538c51e6089ddac124) Add tests for CompositeField (Robbie Averill)
### Bugfixes
* 2018-12-10 [11970d5](https://github.com/silverstripe/silverstripe-graphql/commit/11970d56f3e57f370359cf9edab4c27e9f766c3c) Ensure httpMethod context is applied to all controller actions (#194) (Aaron Carlino)
* 2018-12-04 [a0bb79b](https://github.com/silverstripe/silverstripe-admin/commit/a0bb79b62bc5e907adf8ff93acd0d3fd0f9046f6) Ensure the container exists before unmounting React/removing it (Guy Marriott)
* 2018-12-02 [c28f5ad4](https://github.com/silverstripe/silverstripe-cms/commit/c28f5ad45fb7010b9ad71c3594161d0de15aa763) CMSPageHistoryControllerTest now uses a stub controller to avoid URL conflicts with versioned-admin (Robbie Averill)
* 2018-11-30 [7567488](https://github.com/silverstripe/silverstripe-admin/commit/7567488458daa16d20e6a95d1b33895f7293c69f) Fix new page form clearing when selecting a `Under another page` option (bergice)
* 2018-11-19 [1946aca](https://github.com/silverstripe/silverstripe-admin/commit/1946acab5ab70af8ac61cb419b5ca82336569e4b) Correct the entwine match for the loading animation (Maxime Rainville)
* 2018-11-13 [fa14ecd](https://github.com/silverstripe/recipe-core/commit/fa14ecdad5169cb3ab03ecdddb523f723ded1b27) Ensure that the PasswordValidator is registered with Injector (Robbie Averill)
* 2018-11-08 [2f896b1](https://github.com/silverstripe/recipe-core/commit/2f896b11530cb55acab9bc43d33ce867faafbf3c) Move password complexity requirements into framework (Robbie Averill)
* 2018-11-05 [7fd4a4e](https://github.com/silverstripe/silverstripe-admin/commit/7fd4a4ef76733968ed3babbc14564b2d0f7417b3) Fix duplicate plugins on HTML editor fields (#721) (Luke Edwards)
* 2018-11-05 [4a65d59](https://github.com/silverstripe/silverstripe-admin/commit/4a65d59dc340719f67df1d2c1998ea2645d58473) Fix form changes triggered, GridField add existing (#743) (Luke Edwards)
* 2018-11-02 [97180c261](https://github.com/silverstripe/silverstripe-framework/commit/97180c261258861b5b2b91609a71d044456625d7) Fix readonly grid state always being truthy (#8562) (Luke Edwards)
* 2018-11-02 [12e2cc3](https://github.com/silverstripe/silverstripe-asset-admin/commit/12e2cc37a7a2806310ac7ffd2ad704fb7ad37fe0) Fix duplicate plugins on HTML editor fields (#861) (Luke Edwards)
* 2018-11-02 [d9b1721a](https://github.com/silverstripe/silverstripe-cms/commit/d9b1721ac32829a33f54a9e628680cb3894191b3) Fix duplicate plugins on HTML editor fields (#2307) (Luke Edwards)
* 2018-11-01 [8866e7674](https://github.com/silverstripe/silverstripe-framework/commit/8866e7674a1a9c2be48c8e9532cfcaa667cdf7b5) Fix duplicate plugins on HTML editor fields (#8559) (Luke Edwards)
* 2018-11-01 [55f95b7bc](https://github.com/silverstripe/silverstripe-framework/commit/55f95b7bc8f91384df459bd70c87cacf92225f68) many many through not sorting by join table (#8534) (Michael Strong)
* 2018-10-31 [af7086a](https://github.com/silverstripe/silverstripe-asset-admin/commit/af7086a3adf86b06b6c6b6f938acabf2cfa3352b) Remove outdated CSS Safari hack interfering with the search panel and submit button (Serge Latyntcev)
* 2018-10-31 [2ef7bd29](https://github.com/silverstripe/silverstripe-cms/commit/2ef7bd29754f84fa6eafce08c00d6e1e794713af) IE11+Edge17 Pages tree List View button (Serge Latyntcev)
* 2018-10-30 [3f4d5ae0](https://github.com/silverstripe/silverstripe-cms/commit/3f4d5ae03e9fee10c54f3e628a194d03c07b5c3a) Bypass cached versions to prevent stale state (Aaron Carlino)
* 2018-10-30 [4b0e69a](https://github.com/silverstripe/silverstripe-admin/commit/4b0e69a084028eb60c9d3da90e648acd87946d73) Add aria-expanded to help menu toggle for screenreader accessibility (Sacha Judd)
* 2018-10-30 [2900ac6](https://github.com/silverstripe/silverstripe-admin/commit/2900ac6481b5cca1df1b6708522a60d8a946b790) Remove text-align start with IE supported left (Raissa North)
* 2018-10-29 [f2467d3](https://github.com/silverstripe/silverstripe-admin/commit/f2467d37241ca08b0cc4a112d1dc9054adf891a8) Fix search filtering and clearing (#687) (Luke Edwards)
* 2018-10-26 [3284bf48d](https://github.com/silverstripe/silverstripe-framework/commit/3284bf48d6e3da8b2b1a7831e2d7fe4b401e2fd6) Fix search filtering relations and clear filters (#8477) (Luke Edwards)
* 2018-10-22 [df86335](https://github.com/silverstripe/silverstripe-admin/commit/df863357b1413508f985f6f12a48f5d414a6d75f) Fix decimal search filter not showing up (bergice)
* 2018-10-20 [7f6f5c9ec](https://github.com/silverstripe/silverstripe-framework/commit/7f6f5c9ec9352172f37f8980d823e85c1c39062a) Flush extra methods cache on DataObjects after each unit test class has finished (Robbie Averill)
* 2018-10-19 [311fd62d9](https://github.com/silverstripe/silverstripe-framework/commit/311fd62d9527a47586d90a6f4e2c80922d15d44f) getExtensionInstance can return null, add a case to handle that (Robbie Averill)
* 2018-10-19 [a6855ec](https://github.com/silverstripe/silverstripe-admin/commit/a6855ecf02d41378b1ad03729a103d193aecd853) Remove deprecated help_link definition in testGetHelpLinks (Robbie Averill)
* 2018-10-19 [a28e2e183](https://github.com/silverstripe/silverstripe-framework/commit/a28e2e183e1d0684dd32bc7bcf72d4a9c573a8f4) Fix enum filter in Search component from adding `Any` as a filter (bergice)
* 2018-10-18 [e3d0bcb](https://github.com/silverstripe/silverstripe-admin/commit/e3d0bcb051e45d3e7b90dcdf6554e97d173ef6ce) Change one tab not all tabs (Raissa North)
* 2018-10-17 [87a5d07](https://github.com/silverstripe/silverstripe-admin/commit/87a5d07b9f22b894dd9f4397ff50868e662b79b2) Fix body overflow causing scroll bars (Loz Calver)
* 2018-10-16 [a3d611f](https://github.com/silverstripe/silverstripe-admin/commit/a3d611f0b7f6cd024783a7037245364237329375) Fix `ENTER` not triggering form save button (bergice)
* 2018-10-16 [c35e18110](https://github.com/silverstripe/silverstripe-framework/commit/c35e18110baa72756f2a5378b7e7d4d7803c7c33) Gridfield pagination detected as form change (Luke Edwards)
* 2018-10-16 [5d626fa](https://github.com/silverstripe/silverstripe-admin/commit/5d626fa53b6c67e586d6c6d4d19471709175e8f4) Dont track gridstate changes as form edits (Luke Edwards)
* 2018-10-16 [3d3c407](https://github.com/silverstripe/silverstripe-admin/commit/3d3c407caf099831af0e7b3a6320cad61e5801b0) Fix long gridfield actions overflowing (Luke Edwards)
* 2018-10-15 [ab259af](https://github.com/silverstripe/silverstripe-errorpage/commit/ab259af0707518f94561909c989617e155fd3b1b) Move phpcs to composer dependency, update Travis for it, add 7.2 to Travis (Robbie Averill)
* 2018-10-15 [ab0d7d9](https://github.com/silverstripe/silverstripe-versioned/commit/ab0d7d9e8c18b0e5ae6ee3e352317bbe0c70de53) Fix codesniffer runs in Travis (Robbie Averill)
* 2018-10-11 [0aa2d66](https://github.com/silverstripe/silverstripe-admin/commit/0aa2d6615b7f207791716b6e8654d16940597be4) Use correct lazy loadable class names for GridFieldLazyLoader (Robbie Averill)
* 2018-10-11 [ee21c4201](https://github.com/silverstripe/silverstripe-framework/commit/ee21c42011fd40b2065bb2acb868a427e2232d0a) Re-instate missing SS_DATABASE_SUFFIX functionality (fixes #7966) (Loz Calver)
* 2018-10-11 [4702a22](https://github.com/silverstripe/silverstripe-admin/commit/4702a223ed2c85cd8a55501351526648a70c41b7) Defensively programming some possible failure points (Guy Marriott)
* 2018-10-11 [0db2f84ad](https://github.com/silverstripe/silverstripe-framework/commit/0db2f84ade9b1e8e2811cd7c32bf5f3510544c74) Persist TinyMCE updates when writing with Behat (Guy Marriott)
* 2018-10-10 [e941a56](https://github.com/silverstripe/silverstripe-admin/commit/e941a56b9e918908f16712f2e7be1b43d5810062) Changing the value of a TinyMCE field will correctly trigger a change in the React component (Guy Marriott)
* 2018-10-09 [56d562193](https://github.com/silverstripe/silverstripe-framework/commit/56d56219345b4a8ba318261af98bcd62f3ce060d) Flush extra_methods statics between test runs (Robbie Averill)
* 2018-10-09 [d1281a571](https://github.com/silverstripe/silverstripe-framework/commit/d1281a571a56dca9d40b59a7baf31d32b09a37f5) Escape HTML in PHPDoc to fix API docs from rendering incorrectly (Robbie Averill)
* 2018-10-09 [522b288](https://github.com/silverstripe/silverstripe-admin/commit/522b28890e6b11b3a324a38c65199f96f86c4b2f) ModelAdmin pagination with a filter (Luke Edwards)
* 2018-10-08 [4766cae](https://github.com/silverstripe/silverstripe-admin/commit/4766cae7918e75c3b47d69487fecdb69b2993077) Retain polyfill for display block style in .collapse.show with Bootstrap 4.1.x (Robbie Averill)
* 2018-10-08 [6e649b57](https://github.com/silverstripe/silverstripe-cms/commit/6e649b570d70d83729527ea8fbbc069426f11338) CMSMain::duplicate() now checks canCreate() but not canEdit() (Robbie Averill)
* 2018-10-08 [c4788803e](https://github.com/silverstripe/silverstripe-framework/commit/c4788803ee7b903bc45541ccc0ef8446cf99922f) Remove unused cacheData prop from #8451 (Robbie Averill)
* 2018-10-08 [884a12c](https://github.com/silverstripe/silverstripe-admin/commit/884a12c864dd856a4beb8e636eb56c46be2dfa2e) Add fix for potential tabnabbing on community help links (Sacha Judd)
* 2018-10-08 [979dd38](https://github.com/silverstripe/silverstripe-assets/commit/979dd385947900b9df48928ad7ba4c2eb7a1361f) Fix migrating files with an incorrect class (Luke Edwards)
* 2018-10-08 [fdb53311b](https://github.com/silverstripe/silverstripe-framework/commit/fdb53311bac68bcee5f6b026c0f526c98ea1da65) Fix linting issue. (Maxime Rainville)
* 2018-10-08 [e06bb05](https://github.com/silverstripe/silverstripe-admin/commit/e06bb051d29c9aceb0d6863637bf038ae0715777) Ensure TinyMCE field changes are persisted before updating redux state (Guy Marriott)
* 2018-10-06 [8c7459a70](https://github.com/silverstripe/silverstripe-framework/commit/8c7459a7082ab3880202a3541bd11ed183465ef1) Fix CompositeField test that relied on a DropdownField bug (Sam Minnee)
* 2018-10-05 [e5d3b28a4](https://github.com/silverstripe/silverstripe-framework/commit/e5d3b28a4d10cb4d960897d37071246532ab8ebc) Dont break validation on selects without a source. (Sam Minnee)
* 2018-10-05 [98568262f](https://github.com/silverstripe/silverstripe-framework/commit/98568262f2c5d7cc9a9cd39af158d5df7dce12a7) Fixed phpcs violations (Robbie Averill)
* 2018-10-04 [fafd9dad6](https://github.com/silverstripe/silverstripe-framework/commit/fafd9dad6d60731c0ed6695a2df5535ea433632e) fixing name of constant ASSETS_PATH (Philipp Staender)
* 2018-10-04 [0fc06e51e](https://github.com/silverstripe/silverstripe-framework/commit/0fc06e51e5020b8959310682bade02f97653dc73) Drop seconds from DBDatetime::Nice() to restore SS3 behaviour. (Sam Minnee)
* 2018-10-03 [19af1ac](https://github.com/silverstripe/silverstripe-graphql/commit/19af1ac6d77089a5365ed9f1892306fdd943a2ca) Add codesniffer as a dev dependency and use it in Travis (Robbie Averill)
* 2018-10-03 [4668fab](https://github.com/silverstripe/silverstripe-assets/commit/4668fabc3a02d996a0a9be13245cf3ab3fba1079) Shortcode provider does not always request a protected asset grant, add tests for FlysystemAssetStore (Robbie Averill)
* 2018-10-03 [ce9496d](https://github.com/silverstripe/silverstripe-admin/commit/ce9496d2b9bcda50a4e74c386d31bac7c4dc0939) Quote injector alias references, deprecated and removed support for in Symfony 4 (Robbie Averill)
* 2018-10-03 [d535e71](https://github.com/silverstripe/silverstripe-graphql/commit/d535e71ef81165ac6e02c8b27b1e577b3a291b65) Quote injector alias references, deprecated and removed support for in Symfony 4 (Robbie Averill)
* 2018-10-03 [4740346ed](https://github.com/silverstripe/silverstripe-framework/commit/4740346ed8766549f0f948a4396954227f2494bb) Make ArrayList::limit() consistent with DataList::limit() (Sam Minnee)
* 2018-10-03 [0cc72c91a](https://github.com/silverstripe/silverstripe-framework/commit/0cc72c91ada58d6927dab6e93bfe785b623f3e7a) Use DELETE FROM instead of TRUNCATE for clearTable (Sam Minnee)
* 2018-10-02 [5970fc241](https://github.com/silverstripe/silverstripe-framework/commit/5970fc2417bcd29cfc85c209117b7ed6625141ad) Moving test to correct director (Guy Marriott)
* 2018-10-02 [79c2b5ad4](https://github.com/silverstripe/silverstripe-framework/commit/79c2b5ad427f4e95c8fb51b46c4ba31cdf2997c1) Use DELETE FROM instead of TRUNCATE for clearTable (Sam Minnee)
* 2018-10-01 [f2cbc1dfb](https://github.com/silverstripe/silverstripe-framework/commit/f2cbc1dfbb8b1972eeb72d230f7b5cc2ebad26ee) Dont use USE_FRM in MySQL repair. Fixes #6300. (Sam Minnee)
* 2018-10-01 [638e6ec28](https://github.com/silverstripe/silverstripe-framework/commit/638e6ec2814b4b4cbabd0adc0e166c4812b94740) Throw deprecation notice on limit=0 (Sam Minnee)
* 2018-10-01 [ad87890b2](https://github.com/silverstripe/silverstripe-framework/commit/ad87890b2e92f3f4092bbf9a70ab0d439d40ce31) Dont change state in ArrayList::getIterator() (Sam Minnee)
* 2018-10-01 [63cabc7](https://github.com/silverstripe/silverstripe-assets/commit/63cabc7fc84f295a95e88f2ce37f940b61b97223) Keep folder Name and Title in sync on update (Luke Edwards)
* 2018-10-01 [5c7b0da](https://github.com/silverstripe/silverstripe-admin/commit/5c7b0da18d894d32f3884fcd2f4e18e8ccd7b629) Searching now allows + symbols, use own method over jQuery serialisation (Robbie Averill)
* 2018-10-01 [71dad5f68](https://github.com/silverstripe/silverstripe-framework/commit/71dad5f68518b9052b657c8dc70d4581fb771e98) Append any fields that dont match name in insertBefore/insertAfter (Sam Minnee)
* 2018-10-01 [b0c4c5a1](https://github.com/silverstripe/silverstripe-cms/commit/b0c4c5a1775c95e1abd878f233e78b009f5d01ec) Updating SiteTree search fields to work with new search namespacing (Guy Marriott)
* 2018-10-01 [81292c5](https://github.com/silverstripe/silverstripe-asset-admin/commit/81292c52f04690349ea8d3634398faeda2190f8d) Fix outdated data in Apollo GraphQL cache when deleting/moving files (bergice)
* 2018-09-28 [ac1fe5e9d](https://github.com/silverstripe/silverstripe-framework/commit/ac1fe5e9d5de92dbdd7c03c187471fe6b5d8d7c0) joinClass's default_sort is used when nothing else has been set already (Robbie Averill)
* 2018-09-27 [fa4e031](https://github.com/silverstripe/silverstripe-admin/commit/fa4e031ef961215653e315ea441059c6945e5e3b) Update field names in Behat tests for new namespaces (Robbie Averill)
* 2018-09-27 [44b92c90](https://github.com/silverstripe/silverstripe-cms/commit/44b92c90bc18e629a73584a2a2eb0db8a02d740a) Update field names in Behat tests for new search form namespacing (Robbie Averill)
* 2018-09-27 [c54e7317d](https://github.com/silverstripe/silverstripe-framework/commit/c54e7317d2016727b1e2083996fc925fe862e9ab) Avoid having search fields with the same names as form elements (Guy Marriott)
* 2018-09-27 [2e41ea8](https://github.com/silverstripe/silverstripe-admin/commit/2e41ea83b95509ba1f68cf895c49fd846ec15841) Avoid having search fields with the same name as form elements (Guy Marriott)
* 2018-09-25 [dc59bd8](https://github.com/silverstripe/silverstripe-versioned/commit/dc59bd8e5613442b214f951ca16ec376e1ee1cda) Published GraphQL field now correctly indicates whether the record's version is published (Robbie Averill)
* 2018-09-25 [05b372c](https://github.com/silverstripe/silverstripe-versioned/commit/05b372c85f6720c931ac5dceba5fd05a84c74482) Use Hierarchy::prepopulateTreeDataCache() in CMS. (#183) (Sam Minnée)
* 2018-09-25 [5bfc37ff](https://github.com/silverstripe/silverstripe-cms/commit/5bfc37ff4bd22e8bbc02dc4f6dae59d25a4d5e67) Use Hierarchy::prepopulateTreeDataCache() in CMS (#2266) (Sam Minnée)
* 2018-09-24 [0276f6c08](https://github.com/silverstripe/silverstripe-framework/commit/0276f6c089ff5557a36eaf7367c8fc75fc6af20c) Revert semver break in adding GridField type hint to method signature (Robbie Averill)
* 2018-09-24 [f76fb26](https://github.com/silverstripe/silverstripe-asset-admin/commit/f76fb269b3450077623045ce84726dfd60f92894) fix psr (Thomas Portelange)
* 2018-09-24 [5e069ec](https://github.com/silverstripe/silverstripe-versioned/commit/5e069ec85c3b3cb74054b5cc18012531cfe22ce6) fix inferReciprocalComponent called on unsaved (Thomas Portelange)
* 2018-09-24 [9b5425d](https://github.com/silverstripe/silverstripe-graphql/commit/9b5425d5ba8a25eb799743e62733c57eb2837175) Incorrect parameter order of (Guy Marriott)
* 2018-09-24 [a2bb70c46](https://github.com/silverstripe/silverstripe-framework/commit/a2bb70c46dec00a6c9164bcc134e3fdc64a452e9) Don't flush manifests in tests by default (Ingo Schommer)
* 2018-09-20 [9a89aad](https://github.com/silverstripe/silverstripe-admin/commit/9a89aad5df0d5c67a5575a7e20d723cf9d6c4d95) Whitelist nonce parameters from JS resources to be loaded. (Luke Edwards)
* 2018-09-20 [16b3d18](https://github.com/silverstripe/silverstripe-assets/commit/16b3d18ebdf8896ed216faf2a33daa729c6b2c09) FlysystemAssetStore::getAsURL() only grant for protected filesystems (Christopher Darling)
* 2018-09-20 [a9b2443](https://github.com/silverstripe/silverstripe-admin/commit/a9b244349435028c7b55b30475a9fe4d50207fc1) Revert changes to default dropdownToggleClassNames on ActionMenu (Sacha Judd)
* 2018-09-19 [b98c87a6c](https://github.com/silverstripe/silverstripe-framework/commit/b98c87a6c51baa6696ef9f077775f633c4c5ecd4) Ensure existing session can be accessed if headers_sent() (Sam Minnee)
* 2018-09-17 [d597166](https://github.com/silverstripe/silverstripe-versioned/commit/d5971661cec5fdf12dbfe895fad08ce6bbb05e25) Performance optimisation for draft pages in treeview (Sam Minnee)
* 2018-09-12 [41c0b8fb](https://github.com/silverstripe/silverstripe-cms/commit/41c0b8fb85b7ac11f18eb1813f9e063e13cbafa2) Fix 'Insert links into a page' test (Luke Edwards)
* 2018-09-10 [fb0d81d](https://github.com/silverstripe/silverstripe-admin/commit/fb0d81d6c02bff292e69a0dcd42e3e01be728c01) Remove action menu toggle styles (Sacha Judd)
* 2018-09-04 [fbd8843](https://github.com/silverstripe/silverstripe-assets/commit/fbd88434cfc89eac7d75e34cdcc48f97821198ff) Remove unnecessary UploadTest\Validator (Sam Minnee)
* 2018-09-04 [40c7a0a](https://github.com/silverstripe/silverstripe-assets/commit/40c7a0aac6390237515cd30d9b23de8e7ad0f5ba) Better error message for invalid upload (Sam Minnee)
* 2018-09-03 [641208dc](https://github.com/silverstripe/silverstripe-siteconfig/commit/641208dcd29a7afaf70c3100a1b02a0bd149b667) Text collector translations now compile without errors (Robbie Averill)
* 2018-09-03 [225445931](https://github.com/silverstripe/silverstripe-framework/commit/22544593101ce670a809f3b354f5ff850840006b) Text collector translations now compile without errors (Robbie Averill)
* 2018-08-31 [f5869a5](https://github.com/silverstripe/silverstripe-campaign-admin/commit/f5869a56ba1c34568d424a2cf71000abfa0ef206) Do not render view mode toggle on campaign toolbar if the campaign is empty (bergice)
* 2018-08-30 [5488b31](https://github.com/silverstripe/silverstripe-admin/commit/5488b31f84f47a18d40cf34d7da53d498b169496) Add explicit `0` z-index to `cms-content` so the menu toggle can render above it (#620) (Andre Kiste)
* 2018-08-30 [463fdef](https://github.com/silverstripe/silverstripe-admin/commit/463fdefde0f827d783f9a519f261d1e4d35c01ab) Remove "more" action icon size, add btn-sm and fix icon alignment in gridfield (Sacha Judd)
* 2018-08-28 [dbfc25302](https://github.com/silverstripe/silverstripe-framework/commit/dbfc253021bce3997af0934b9015215047bbac7b) Fix incorrect version number in 4.3.0 changelog (Loz Calver)
* 2018-08-28 [d1951c94](https://github.com/silverstripe/silverstripe-cms/commit/d1951c946fe143e79ea6a7e1ee55ae90586c8a33) Sort history viewer versions in descending order (Robbie Averill)
* 2018-08-28 [10ef38f](https://github.com/silverstripe/silverstripe-campaign-admin/commit/10ef38f039fa4bf29be764ffd99196b0f9b62554) Hide 1px left border in preview component if we are in 'Preview Only' mode (bergice)
* 2018-08-27 [2ab622f](https://github.com/silverstripe/silverstripe-admin/commit/2ab622f88a5a179a7186b46699055cf761d3b749) Fix Add mock store to the loadComponent AppolloProvider (Maxime Rainville)
* 2018-08-24 [e196475](https://github.com/silverstripe/silverstripe-assets/commit/e196475220e1b97cc61f8e026b55984d2e240e0d) Graceful validation of image shortcode (Aaron Carlino)
* 2018-08-24 [2b16e2a](https://github.com/silverstripe/silverstripe-admin/commit/2b16e2afbaa322ef4ab2fce6c2add9b8f5596ba4) GridField delete button to offer archive action if possible (#602) (Luke Edwards)
* 2018-08-24 [6164d01d6](https://github.com/silverstripe/silverstripe-framework/commit/6164d01d65648ce6b25a7ef82fabaa10b81565d0) GridField delete button to offer archive action if possible (#8325) (Luke Edwards)
* 2018-08-22 [1b67bb08c](https://github.com/silverstripe/silverstripe-framework/commit/1b67bb08c8b61ad7e5324ef07eaea2834772b818) Fix failing HTML button test step (Luke Edwards)
* 2018-08-17 [160d595e2](https://github.com/silverstripe/silverstripe-framework/commit/160d595e226edcbaa64a47a0be74193a8b8058cc) fix trailing whitespace (maks)
* 2018-08-17 [16217f365](https://github.com/silverstripe/silverstripe-framework/commit/16217f3655c28ddcf6a721bca82d45d65b91e3ed) fix accidentaly deleted comma (maks)
* 2018-08-16 [61c046c](https://github.com/silverstripe/silverstripe-versioned/commit/61c046c9cd6ce97456b6123a05438c7cd05d07cc) If archive's possible switch GridField delete button with archive (Luke Edwards)
* 2018-08-15 [d9154bffb](https://github.com/silverstripe/silverstripe-framework/commit/d9154bffbf7b0031e5bd3ed1f68db3fae6ab5959) text/json is not a valid mimetype (Daniel Hensby)
* 2018-08-15 [d18b5ee](https://github.com/silverstripe/silverstripe-campaign-admin/commit/d18b5eed63e081a4cbcbb30edcf51839a2ae3461) text/json is not a valid mimetype (Daniel Hensby)
* 2018-08-15 [41a2a0c](https://github.com/silverstripe/silverstripe-admin/commit/41a2a0c38c073d82b96fd4fa2fa09bea3b556aa5) text/json is not a mimetype (Daniel Hensby)
* 2018-08-14 [fcaa9ba](https://github.com/silverstripe/silverstripe-versioned/commit/fcaa9ba7a68a839d84ff23d32275c510d0f9890e) Restore and archive action improvements (Luke Edwards)
* 2018-08-14 [fc7f712](https://github.com/silverstripe/silverstripe-admin/commit/fc7f7120a67ce95b03f13b4c8fb90b36f810f7b1) Modal response animation appearing outside the modal (#601) (Luke Edwards)
* 2018-08-07 [c2b54c7](https://github.com/silverstripe/silverstripe-admin/commit/c2b54c72a990df9d453bedc307f613f23107bfad) graphql route getting overwritten (Aaron Carlino)
* 2018-08-06 [e7cb0156](https://github.com/silverstripe/silverstripe-cms/commit/e7cb0156c69a3701b248dbbae4e72f8c0b372efd) Use LatestDraftVersion in GraphQL query to determine latest draft version (Robbie Averill)
* 2018-08-06 [13372f9a3](https://github.com/silverstripe/silverstripe-framework/commit/13372f9a37d1cb19f658404c79c2be6fbfa557b1) Installer redirect to home/ (without domain) (Michael Strong)
* 2018-08-02 [24927c5](https://github.com/silverstripe/silverstripe-campaign-admin/commit/24927c5aa18f9adc8ef79f0adf879f6bcd5c130c) Ensure only toolbar buttons that are immediate descendants of toolbars are given margins (Robbie Averill)
* 2018-08-01 [a981584](https://github.com/silverstripe/silverstripe-admin/commit/a9815845c0e923587fa81bdbac77be43f6d4dd1a) Remove rogue CSS margin on toolbar buttons. Implemented in campaign-admin preview instead. (Robbie Averill)
* 2018-08-01 [405d8a3](https://github.com/silverstripe/silverstripe-campaign-admin/commit/405d8a3213852f4a35e7cf8101df72c206d1b2f9) Toolbar button margins are constrained to campaign previews, and update ViewModeActions name (Robbie Averill)
* 2018-08-01 [6889a1a](https://github.com/silverstripe/silverstripe-admin/commit/6889a1adf0005e91288c4e2ddc6a2f3ea1b6b593) ViewModeToggle now uses BEM class naming convention (Robbie Averill)
* 2018-07-30 [420c3f8](https://github.com/silverstripe/silverstripe-asset-admin/commit/420c3f807d7426af2c76d778faa4ef26ab5dda11) Editor should ignore drag-and-drop files (#814) (Luke Edwards)
* 2018-07-30 [fde7b9ddc](https://github.com/silverstripe/silverstripe-framework/commit/fde7b9ddc5da697395897249819b3c52530692b6) Specify minimum composer version (Maxime Rainville)
* 2018-07-27 [67254da1](https://github.com/silverstripe/silverstripe-reports/commit/67254da18599f0fe86921098524ec3303d9de41e) Apply missing class to report header. (Maxime Rainville)
* 2018-07-26 [900ca9c8d](https://github.com/silverstripe/silverstripe-framework/commit/900ca9c8d75b70b13b425365022ec3f1f0ebe461) Recommend install of upgrader with PHAR exec. (Maxime Rainville)
* 2018-07-25 [0035f4a90](https://github.com/silverstripe/silverstripe-framework/commit/0035f4a90728d9e109b12585b32491a2afeaa916) Fix backtick in changelog breaking sentence formatting (Michal Kleiner)
* 2018-07-13 [d1024ee](https://github.com/silverstripe/silverstripe-assets/commit/d1024ee00b12c3a212fe12d168f4521e2188274b) Add HTMLFragment casting to $Tag (#148) (Jake Bentvelzen)
* 2018-07-12 [a8e5616](https://github.com/silverstripe/silverstripe-admin/commit/a8e56166f01ef0fb8ecd46edb4eddde30447cdb0) Update GridField.js so it works with new Archive View (#559) (Luke Edwards)
* 2018-07-12 [599a4420b](https://github.com/silverstripe/silverstripe-framework/commit/599a4420bf0d982343faa6145afaf6592566bb40) Improve GridFieldViewButton to work with new Archive Admin (#8240) (Luke Edwards)
* 2018-07-11 [c2347310](https://github.com/silverstripe/silverstripe-cms/commit/c23473103e4f93b82d60f1260923c9e413d02c41) URLSegment field styling fixes #2193 (Maxime Rainville)
* 2018-07-05 [91068c23b](https://github.com/silverstripe/silverstripe-framework/commit/91068c23b5cb448fe63ae9f40875a8f0818dbe1f) Make column query not distinct (Al Twohill)
* 2018-07-05 [730fc42](https://github.com/silverstripe/silverstripe-admin/commit/730fc42ef3e6d25345516f1583d05bf968bf762c) Fix test for password recovery (Ingo Schommer)
* 2018-07-01 [3262665b2](https://github.com/silverstripe/silverstripe-framework/commit/3262665b2d6bb6d56186be2e2e370b853f13c6b5) Fix link and turn of phrase. (Maxime Rainville)
* 2018-06-29 [cc9b36e01](https://github.com/silverstripe/silverstripe-framework/commit/cc9b36e01124349ea7ccbd0902d1b01c764f82f7) fix link (Lukas)
* 2018-06-22 [f9de357](https://github.com/silverstripe/silverstripe-admin/commit/f9de35724c4fb75c8a6d38e4f5b9185531fc961c) - Grid field headers misaligned (Petar Simic)
* 2018-06-18 [2f1c2992f](https://github.com/silverstripe/silverstripe-framework/commit/2f1c2992f8f61b4a87a0a363db289a19ac5a821b) Default cache state should be `no-cache` (Daniel Hensby)
* 2018-06-17 [25b0a18](https://github.com/silverstripe/silverstripe-admin/commit/25b0a18a743e81951a2d2df387b6e5442d0253c3) Fix display of GridField link existing button (Luke Edwards)
* 2018-06-15 [5e4ad34](https://github.com/silverstripe/silverstripe-installer/commit/5e4ad341622565cc998bd8537ad3ec7a6a6a7913) Fix incorrect base recipe dependency (Damian Mooyman)

View File

@ -0,0 +1,33 @@
# 4.4.0
## Overview {#overview}
- [Correct PHP types are now returned from database queries](/developer_guides/model/sql_select#data-types)
- The name of the directory where vendor module resources are exposed can now be configured by defining a `extra.resources-dir` key in your `composer.json` file. If the key is not set, it will automatically default to `resources`. New projects will be preset to `_resources`. This will avoid potential conflict with SiteTree URL Segments.
- dev/build is now non-destructive for all Enums, not just ClassNames. This means your data won't be lost if you're switching between versions, but watch out for code that breaks when it sees an unrecognised value!
## Upgrading {#upgrading}
### Adopting to new `_resources` directory
1. Update your `.gitignore` file to ignore the new `_resources` directory. This file is typically located in the root of your project or in the `public` folder.
2. Add a new `extra.resources-dir` key to your composer file.
```js
{
// ...
"extra": {
// ...
"resources-dir": "_resources"
}
}
```
3. Expose your vendor assets by running `composer vendor-expose`.
4. Remove the old `resources` folder. This folder will be located in the `public` folder if you have adopted the public web root, or in the root of your project if you haven't.
You may also need to update your server configuration if you have applied special conditions to the `resources` path.
## Changes to internal APIs
- `PDOQuery::__construct()` now has a 2nd argument. If you have subclassed PDOQuery and overridden __construct()
you may see an E_STRICT error
- The name of the directory where vendor module resources are exposed can now be configured by adding a `extra.resources-dir` key to your composer file. The new default in `silverstripe/installer` has been changed to `_resources` rather than `resources`. This allows you to use `resources` as a URL segment or a route.

View File

@ -0,0 +1,295 @@
# 4.3.0-rc1
<!--- Changes below this line will be automatically regenerated -->
## Change Log
### Security
* 2018-10-24 [88391f2](https://github.com/silverstripe/silverstripe-graphql/commit/88391f27463cac553360d7d94b0760e797247855) CSRF protection (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-08-21 [3dbb10625](https://github.com/silverstripe/silverstripe-framework/commit/3dbb10625c6918094a18cd9a29f1f9daca8129c5) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019)
* 2018-08-12 [3424431](https://github.com/silverstripe/silverstripe-admin/commit/3424431a5ed52fc9aae97423268aea5eba7334a2) Add CSRF to Apollo (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
* 2018-07-29 [637b4225c](https://github.com/silverstripe/silverstripe-framework/commit/637b4225c6a85bfa0d59e516a8c602203cc980d9) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018)
### API Changes
* 2018-10-23 [d9e8341](https://github.com/silverstripe/silverstripe-versioned-admin/commit/d9e83412a309cb0f03cdd50a218f4dc45b470821) Deprecate HistoryControllerFactory and default to CMSPageHistoryV… (#71) (Maxime Rainville)
* 2018-10-05 [5276b6cbb](https://github.com/silverstripe/silverstripe-framework/commit/5276b6cbb1ebd49d704734d940b0467c51b0d064) Add FieldList::getContainerField (Maxime Rainville)
* 2018-10-04 [dba237e](https://github.com/silverstripe/silverstripe-admin/commit/dba237e5d6ab13a7a5f8cbc9a9e594d98478c566) Allow Gridfield to be lazy loadable. (Maxime Rainville)
* 2018-10-04 [bdb53979a](https://github.com/silverstripe/silverstripe-framework/commit/bdb53979aaed743f278b299e3dab7b13e321fbe7) Create a new GridFieldLazyLoader GridField component (Maxime Rainville)
* 2018-09-28 [cb2b9498f](https://github.com/silverstripe/silverstripe-framework/commit/cb2b9498fb33bd57a19ba6a7ceed6d3fbc976d70) Deprecate updateSearchContextCallback and updateSearchFormCallback (Robbie Averill)
* 2018-09-03 [f48ab77](https://github.com/silverstripe/silverstripe-assets/commit/f48ab777c2845d7b414ebae0ff7903c6bb423340) Add `show_file_link_tracking` config to `FileLinkTracking` extension to control visibility of the File Tracking tab (bergice)
* 2018-08-31 [01db5c9e9](https://github.com/silverstripe/silverstripe-framework/commit/01db5c9e98f5406e8d6b0f1f4ea660edb24b199f) Add Link Tracking section to Relations developer guide and describe `show_sitetree_link_tracking`, `show_file_link_tracking`. (bergice)
* 2018-08-31 [115ed92e](https://github.com/silverstripe/silverstripe-cms/commit/115ed92e0aed63df1620700955afd6248fa97fae) Add `show_sitetree_link_tracking` config to `SiteTreeLinkTracking` extension to control visibility of the Link Tracking tab (bergice)
* 2018-08-05 [02be4cc](https://github.com/silverstripe/silverstripe-graphql/commit/02be4cc3e1cab7c1ed8e8f9d6dde6ee102364fa6) Multi-schema support (#169) (Aaron Carlino)
* 2018-07-31 [47e3ae2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/47e3ae27ea451f769c5e90f4c31589a5265c337c) Versions from and to are now stored as objects in the store (#29) (Dylan Wagstaff)
* 2018-07-12 [0343203](https://github.com/silverstripe/silverstripe-versioned/commit/03432030e99a496c6cd81f4bec780164851aa955) Add new RestoreAction and canRestoreToDraft method (#168) (Luke Edwards)
### Features and Enhancements
* 2018-11-01 [2f2fb2b](https://github.com/silverstripe/silverstripe-admin/commit/2f2fb2bfdcd88b54ac9426f172bec77a3c210172) Improve loading screen and indicator (#582) (Luke Edwards)
* 2018-10-29 [e51be58](https://github.com/silverstripe/silverstripe-asset-admin/commit/e51be58685527b512bf20e7542d506aef14f4ed8) Move Remove button into preview-image message (#859) (Sacha Judd)
* 2018-10-29 [2440432](https://github.com/silverstripe/silverstripe-asset-admin/commit/24404324790a26b0c19f68f862bb2679618199ee) Move the replace file into the more options action set (#848) (Sacha Judd)
* 2018-10-29 [952f37a](https://github.com/silverstripe/silverstripe-admin/commit/952f37abdc78d9f87da20461537c88cf570d7cba) Adding an HOC to provide DragDropContext for consumers of ReactDND (#711) (Guy Marriott)
* 2018-10-26 [2c3665d](https://github.com/silverstripe/silverstripe-admin/commit/2c3665d83375364249adf02c2eef6382d73e7c43) Adding a component for a generic popover filled with buttons (#684) (Guy Marriott)
* 2018-10-19 [e88b8e7](https://github.com/silverstripe/silverstripe-admin/commit/e88b8e7b2fa4c47e62d737ea3a3ccee8c7ff8794) Expose TabsActions (#703) (Raissa North)
* 2018-10-18 [761a6f7](https://github.com/silverstripe/silverstripe-admin/commit/761a6f7f6f809c36f21dd804ce1d6325909b5d1c) Reverse argument signature of methods using path (#698) (Aaron Carlino)
* 2018-10-17 [902fec0](https://github.com/silverstripe/silverstripe-asset-admin/commit/902fec0f89bb49783b2dc5f64b4d0c24c33531fb) Extensible readFiles query (#847) (Aaron Carlino)
* 2018-10-17 [437e53f2f](https://github.com/silverstripe/silverstripe-framework/commit/437e53f2fec17bc778cc7bba39de322d43214441) Some minor refactoring of the PDO and MySQLi connectors (Robbie Averill)
* 2018-10-15 [3e2ce31](https://github.com/silverstripe/silverstripe-admin/commit/3e2ce3138d88674fc27aa465dc9be9ea4533c830) Add nested fields, args, distribute args to fields (#683) (Aaron Carlino)
* 2018-10-08 [2775895d0](https://github.com/silverstripe/silverstripe-framework/commit/2775895d03af93d207079637a8eb6afdfad5ab01) Adding a helper to find a form field by label content (Guy Marriott)
* 2018-10-05 [6e66c48](https://github.com/silverstripe/silverstripe-admin/commit/6e66c48d65b52d568fbd34f8a7c630b4a829c2c9) Add CMS community help menu to cms-menu (#615) (Sacha Judd)
* 2018-10-04 [9ea7b58a8](https://github.com/silverstripe/silverstripe-framework/commit/9ea7b58a8f26ca8856211da30eed5751706d0c4b) Add memory cache to ThemeResourceLoader::findTemplate() (Robbie Averill)
* 2018-10-03 [90afb2037](https://github.com/silverstripe/silverstripe-framework/commit/90afb2037a6d2d9be3f605aad9ddfbda0bbab2e7) TabSet react component is no longer structural (Raissa North)
* 2018-10-03 [0be4a9a](https://github.com/silverstripe/silverstripe-admin/commit/0be4a9abfa4482deb2753c50c47dbf0192581a12) Adding an extension point to FormBuilderLoader after redux form is initialised (Raissa North)
* 2018-10-02 [cba5d30](https://github.com/silverstripe/silverstripe-admin/commit/cba5d307f499471b88f77fd201df2a4baacf0038) Connect Tabs component to redux-form to handle activeTab state (Raissa North)
* 2018-10-02 [e1f2f89d3](https://github.com/silverstripe/silverstripe-framework/commit/e1f2f89d37e44c706d350d02774733dd867ccdc7) Add test for PHP 7.3 support (SS4 version) (Sam Minnee)
* 2018-09-28 [bd37b90a](https://github.com/silverstripe/silverstripe-cms/commit/bd37b90a3a5811555248ef616698efbe24466b11) Add CMSMain.enable_archive_warning_message config (Sam Minnee)
* 2018-09-27 [4b155b9](https://github.com/silverstripe/silverstripe-graphql/commit/4b155b982ca6fd737aa70e281ce679bb43404060) Add getSortableFields to return sortable fields for query (#185) (Robbie Averill)
* 2018-09-27 [25759ffc5](https://github.com/silverstripe/silverstripe-framework/commit/25759ffc5fe532a239b1487ca6b025140d2e144f) Show file path on PHP parser exceptions (Ingo Schommer)
* 2018-09-25 [5b7a84141](https://github.com/silverstripe/silverstripe-framework/commit/5b7a84141b0fbef66a9f3f52a9ccee12e02ef1e0) Add Hierarchy::prepopulate_numchildren_cache() (#8380) (Sam Minnée)
* 2018-09-21 [e2701a4](https://github.com/silverstripe/silverstripe-admin/commit/e2701a4cd00ffc2136b935038d8422a25acfa2dd) TinyMCE inline toolbar for images and embeds (Luke Edwards)
* 2018-09-20 [c928e83](https://github.com/silverstripe/silverstripe-versioned/commit/c928e830825ec8ae5336d254835f754a11bac350) Backport of DataList for list of versions, for SS 4.3.x (Robbie Averill)
* 2018-09-19 [d9a6c10](https://github.com/silverstripe/silverstripe-admin/commit/d9a6c10a157d6c1fd17351b0097ce9d8c5336ecf) Form state & schema persists across form remounting (Guy Marriott)
* 2018-09-19 [40dde226f](https://github.com/silverstripe/silverstripe-framework/commit/40dde226fd1b8997308ef0f5718763a298295cdf) Add ?showqueries=backtrace (Sam Minnee)
* 2018-09-19 [6de12e1](https://github.com/silverstripe/silverstripe-asset-admin/commit/6de12e1375d146d61052f1a6dd001aa3a6c437bc) TinyMCE inline toolbar for images and embeds (Luke Edwards)
* 2018-09-17 [588bf83e1](https://github.com/silverstripe/silverstripe-framework/commit/588bf83e1238a79793da4bc4145b7597ae2626be) Add hideNav flag to schema defaults (Raissa North)
* 2018-09-17 [a800ce7](https://github.com/silverstripe/silverstripe-admin/commit/a800ce714a99a631b8a7401322df0653e2ee476c) Add hideNav flag to allow hiding of navigation in Tabs (Raissa North)
* 2018-09-12 [d8bf873](https://github.com/silverstripe/silverstripe-admin/commit/d8bf873e1236ff5cb634cce0a47c992366ae6139) Use bootstrap modal footer, use our icon font for close icon (Luke Edwards)
* 2018-09-12 [c03c685](https://github.com/silverstripe/silverstripe-asset-admin/commit/c03c685da247b8baa6d4180816bda35bc29fcf2d) Use bootstrap modal footer, use our icon font for close icon (Luke Edwards)
* 2018-09-03 [2394194](https://github.com/silverstripe/silverstripe-admin/commit/239419424ee001200830bc8b77dfef3ce7a05d7e) HtmlEditorField component for react rich text (Dylan Wagstaff)
* 2018-08-28 [b5d322d](https://github.com/silverstripe/silverstripe-versioned-admin/commit/b5d322d277efd1fec846f6fca3a92d317542a7ae) HistoryViewerField now uses schemaData to inject necessary input props (Robbie Averill)
* 2018-08-24 [2b335b4](https://github.com/silverstripe/silverstripe-graphql/commit/2b335b4239946f9a6fb1d525452cf1fe6d22a9ce) Proof of concept of cached graphql queries (#166) (Damian Mooyman)
* 2018-08-22 [a257c1c](https://github.com/silverstripe/silverstripe-admin/commit/a257c1cea84ebcdd27df5043c3690bdcb09f43da) Add toggleCallback function (Raissa North)
* 2018-08-22 [8153d12](https://github.com/silverstripe/silverstripe-admin/commit/8153d12537b53bd208ddb28675aa82bd84da2a16) Add dropdownToggleClassName prop to ActionMenu (Raissa North)
* 2018-08-20 [26262ea](https://github.com/silverstripe/silverstripe-admin/commit/26262ea3a9d64e4172ed277845a53e58c48772ef) Insert link shortcut for HTMLEditorField (#599) (Luke Edwards)
* 2018-08-12 [8d3b022](https://github.com/silverstripe/silverstripe-assets/commit/8d3b02259167ce08fd30742475a29219d64df0c8) Support setting quality on a per-image basis (#153) (Loz Calver)
* 2018-08-05 [9d0ae97](https://github.com/silverstripe/silverstripe-admin/commit/9d0ae970e5cdf2ec8710b471884d75b5485b9309) ViewModeToggle states are now stored in constants (Robbie Averill)
* 2018-08-01 [4213eeb](https://github.com/silverstripe/silverstripe-asset-admin/commit/4213eeb1513662ef2dd177065310c2062532a424) Use the new general purpose search component. (#812) (Maxime Rainville)
* 2018-08-01 [5a00d84](https://github.com/silverstripe/silverstripe-admin/commit/5a00d8462ec2806c640f1214971f4747f276d398) General purpose search form component (#572) (Maxime Rainville)
* 2018-07-30 [163ca65](https://github.com/silverstripe/silverstripe-graphql/commit/163ca65a1f2e858c7d47d4b15775153ea0451d5b) DataObjectScaffolder instantiation is now handled through Injector (Robbie Averill)
* 2018-07-30 [24d3023](https://github.com/silverstripe/silverstripe-graphql/commit/24d3023b0d361fee66b4e68aca1814b7dfdbcac2) Allow non-internal input types passed as args (#168) (Aaron Carlino)
* 2018-07-25 [79a5ea3](https://github.com/silverstripe/recipe-cms/commit/79a5ea3dace336558ef4d5be4a86cc1a0e84badc) Add versioned-admin (Luke Edwards)
* 2018-07-24 [dee3fc2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/dee3fc286fc7448e24222640167e37ae95ec705b) History Viewer now uses ViewModeToggle to control the preview panel (Robbie Averill)
* 2018-07-24 [064bb9a](https://github.com/silverstripe/silverstripe-versioned-admin/commit/064bb9a6544a78212ba23b946b57454ebed4dbdb) Add behat tests for using compare mode (Robbie Averill)
* 2018-07-23 [a8e0d63](https://github.com/silverstripe/silverstripe-versioned-admin/commit/a8e0d6394456b00a41bf815b1a6c50a6a09e49a2) styles and fixes for styling and code cleanliness (Robbie Averill)
* 2018-07-19 [e029b73](https://github.com/silverstripe/silverstripe-versioned-admin/commit/e029b73106ef9bffa53ba38822e3b5a1ba975cdf) Alter components to allow for compare mode (Dylan Wagstaff)
* 2018-07-19 [75c2c85](https://github.com/silverstripe/silverstripe-versioned-admin/commit/75c2c8572a32139dce267976aa58fb0d765d1a83) Add schema endpoint and shell method for returning compareForm (Robbie Averill)
* 2018-07-18 [db72f6d](https://github.com/silverstripe/silverstripe-versioned-admin/commit/db72f6d1149eabbc902f0725a75c38858e464210) Adjust history viewer to allow for compare mode (Dylan Wagstaff)
* 2018-07-18 [359a260](https://github.com/silverstripe/silverstripe-versioned-admin/commit/359a260ddba5d29fdbc22b154897dd45c0797c3f) Dropdown atop history viewer for holding actions (Raissa North)
* 2018-07-18 [11ea083](https://github.com/silverstripe/silverstripe-versioned-admin/commit/11ea083ba1749722e3fb76685e602e23f7f301ad) Adding a compare mode active notice to the history viewer (Guy Marriott)
* 2018-07-17 [337da78](https://github.com/silverstripe/silverstripe-campaign-admin/commit/337da782bd520beebdb734542b5403b69257d058) Update webpack-config constraint (Raissa North)
* 2018-07-16 [786446fb](https://github.com/silverstripe/silverstripe-reports/commit/786446fb670905832b6bfe49775b9c2eaff262cc) Use Injector to create new class instances and pass $params (Robbie Averill)
* 2018-07-12 [f2ebdb7f](https://github.com/silverstripe/silverstripe-cms/commit/f2ebdb7f5ec894e8c0c8f29b9aac984aef4ca8ed) add SiteTree::updateAnchorsOnPage() for user defining additional page anchors (Will Rossiter)
* 2018-07-12 [114b0a5ea](https://github.com/silverstripe/silverstripe-framework/commit/114b0a5ea7ea6b8f33b8c9b8d1611e5ee6619a1c) Option for secure "remember me" cookie (Ingo Schommer)
* 2018-07-12 [05a6c17](https://github.com/silverstripe/silverstripe-versioned-admin/commit/05a6c17bd3330fe85a74a59c3dcc4a75fc88b75c) Archive admin for managing archived records (#29) (Luke Edwards)
* 2018-07-12 [3292a8b77](https://github.com/silverstripe/silverstripe-framework/commit/3292a8b773c5b29a69b72718f996a36f3daead1d) Add `columnUnique` API SS_List classes. (Al Twohill)
* 2018-07-09 [0fc7660](https://github.com/silverstripe/silverstripe-versioned-admin/commit/0fc766020c1f970c42a173016b98e7dc865516aa) Add diff view form transform for comparisons (Dylan Wagstaff)
* 2018-07-06 [6c1a34c](https://github.com/silverstripe/silverstripe-campaign-admin/commit/6c1a34cc2c9bfbc3bca471c7ca77976bac194918) Make use of ViewModeToggle component. (Raissa North)
* 2018-07-01 [73d3da2](https://github.com/silverstripe/silverstripe-admin/commit/73d3da2bc8566cb1cb5da0124b7deb513728b5ab) Pattern library now has FormAction examples (Robbie Averill)
* 2018-04-15 [5108734](https://github.com/silverstripe/silverstripe-admin/commit/5108734b19edbe9d6404b747e25059a586b009fe) Add ViewModeToggle component (Raissa North)
* 2018-01-30 [1857f00](https://github.com/silverstripe/silverstripe-admin/commit/1857f00bd2b172acb933064d3d454e91bc855200) Add tests for Form component (Robbie Averill)
* 2018-01-30 [cc945f0](https://github.com/silverstripe/silverstripe-admin/commit/cc945f09aac38dea25ec57538c51e6089ddac124) Add tests for CompositeField (Robbie Averill)
### Bugfixes
* 2018-11-05 [7fd4a4e](https://github.com/silverstripe/silverstripe-admin/commit/7fd4a4ef76733968ed3babbc14564b2d0f7417b3) Fix duplicate plugins on HTML editor fields (#721) (Luke Edwards)
* 2018-11-05 [4a65d59](https://github.com/silverstripe/silverstripe-admin/commit/4a65d59dc340719f67df1d2c1998ea2645d58473) Fix form changes triggered, GridField add existing (#743) (Luke Edwards)
* 2018-11-02 [97180c261](https://github.com/silverstripe/silverstripe-framework/commit/97180c261258861b5b2b91609a71d044456625d7) Fix readonly grid state always being truthy (#8562) (Luke Edwards)
* 2018-11-02 [12e2cc3](https://github.com/silverstripe/silverstripe-asset-admin/commit/12e2cc37a7a2806310ac7ffd2ad704fb7ad37fe0) Fix duplicate plugins on HTML editor fields (#861) (Luke Edwards)
* 2018-11-02 [d9b1721a](https://github.com/silverstripe/silverstripe-cms/commit/d9b1721ac32829a33f54a9e628680cb3894191b3) Fix duplicate plugins on HTML editor fields (#2307) (Luke Edwards)
* 2018-11-01 [8866e7674](https://github.com/silverstripe/silverstripe-framework/commit/8866e7674a1a9c2be48c8e9532cfcaa667cdf7b5) Fix duplicate plugins on HTML editor fields (#8559) (Luke Edwards)
* 2018-11-01 [55f95b7bc](https://github.com/silverstripe/silverstripe-framework/commit/55f95b7bc8f91384df459bd70c87cacf92225f68) many many through not sorting by join table (#8534) (Michael Strong)
* 2018-10-31 [af7086a](https://github.com/silverstripe/silverstripe-asset-admin/commit/af7086a3adf86b06b6c6b6f938acabf2cfa3352b) Remove outdated CSS Safari hack interfering with the search panel and submit button (Serge Latyntcev)
* 2018-10-31 [2ef7bd29](https://github.com/silverstripe/silverstripe-cms/commit/2ef7bd29754f84fa6eafce08c00d6e1e794713af) IE11+Edge17 Pages tree List View button (Serge Latyntcev)
* 2018-10-30 [3f4d5ae0](https://github.com/silverstripe/silverstripe-cms/commit/3f4d5ae03e9fee10c54f3e628a194d03c07b5c3a) Bypass cached versions to prevent stale state (Aaron Carlino)
* 2018-10-30 [4b0e69a](https://github.com/silverstripe/silverstripe-admin/commit/4b0e69a084028eb60c9d3da90e648acd87946d73) Add aria-expanded to help menu toggle for screenreader accessibility (Sacha Judd)
* 2018-10-30 [2900ac6](https://github.com/silverstripe/silverstripe-admin/commit/2900ac6481b5cca1df1b6708522a60d8a946b790) Remove text-align start with IE supported left (Raissa North)
* 2018-10-29 [f2467d3](https://github.com/silverstripe/silverstripe-admin/commit/f2467d37241ca08b0cc4a112d1dc9054adf891a8) Fix search filtering and clearing (#687) (Luke Edwards)
* 2018-10-26 [3284bf48d](https://github.com/silverstripe/silverstripe-framework/commit/3284bf48d6e3da8b2b1a7831e2d7fe4b401e2fd6) Fix search filtering relations and clear filters (#8477) (Luke Edwards)
* 2018-10-24 [e72fc9e3d](https://github.com/silverstripe/silverstripe-framework/commit/e72fc9e3d0f35a1d43f55f83f9919f67d72fb7cb) DataObject singleton creation (#8516) (Sam Minnée)
* 2018-10-22 [df86335](https://github.com/silverstripe/silverstripe-admin/commit/df863357b1413508f985f6f12a48f5d414a6d75f) Fix decimal search filter not showing up (bergice)
* 2018-10-20 [7f6f5c9ec](https://github.com/silverstripe/silverstripe-framework/commit/7f6f5c9ec9352172f37f8980d823e85c1c39062a) Flush extra methods cache on DataObjects after each unit test class has finished (Robbie Averill)
* 2018-10-19 [311fd62d9](https://github.com/silverstripe/silverstripe-framework/commit/311fd62d9527a47586d90a6f4e2c80922d15d44f) getExtensionInstance can return null, add a case to handle that (Robbie Averill)
* 2018-10-19 [a6855ec](https://github.com/silverstripe/silverstripe-admin/commit/a6855ecf02d41378b1ad03729a103d193aecd853) Remove deprecated help_link definition in testGetHelpLinks (Robbie Averill)
* 2018-10-19 [7c65916](https://github.com/silverstripe/silverstripe-asset-admin/commit/7c659167f2eda63d882a097f2f413b9f3cb79e31) Use fixtured file title in test assertion (Robbie Averill)
* 2018-10-19 [a28e2e183](https://github.com/silverstripe/silverstripe-framework/commit/a28e2e183e1d0684dd32bc7bcf72d4a9c573a8f4) Fix enum filter in Search component from adding `Any` as a filter (bergice)
* 2018-10-18 [e3d0bcb](https://github.com/silverstripe/silverstripe-admin/commit/e3d0bcb051e45d3e7b90dcdf6554e97d173ef6ce) Change one tab not all tabs (Raissa North)
* 2018-10-17 [87a5d07](https://github.com/silverstripe/silverstripe-admin/commit/87a5d07b9f22b894dd9f4397ff50868e662b79b2) Fix body overflow causing scroll bars (Loz Calver)
* 2018-10-17 [d71ee0c](https://github.com/silverstripe/silverstripe-admin/commit/d71ee0ce9898e73c9a7d913356fc6bfe6c2b42fc) Fixes #674 TinyMCE width - this should match form field widths at lower width resolutions but expand up to the max width on wider resolutions (bergice)
* 2018-10-16 [a3d611f](https://github.com/silverstripe/silverstripe-admin/commit/a3d611f0b7f6cd024783a7037245364237329375) Fix `ENTER` not triggering form save button (bergice)
* 2018-10-16 [c35e18110](https://github.com/silverstripe/silverstripe-framework/commit/c35e18110baa72756f2a5378b7e7d4d7803c7c33) Gridfield pagination detected as form change (Luke Edwards)
* 2018-10-16 [5d626fa](https://github.com/silverstripe/silverstripe-admin/commit/5d626fa53b6c67e586d6c6d4d19471709175e8f4) Dont track gridstate changes as form edits (Luke Edwards)
* 2018-10-16 [a6a174399](https://github.com/silverstripe/silverstripe-framework/commit/a6a17439976710b2311558d363b5467fa429dcca) Fix `ENTER` not triggering form save button as `GridField`s used `submit` type buttons (bergice)
* 2018-10-16 [3d3c407](https://github.com/silverstripe/silverstripe-admin/commit/3d3c407caf099831af0e7b3a6320cad61e5801b0) Fix long gridfield actions overflowing (Luke Edwards)
* 2018-10-15 [ab259af](https://github.com/silverstripe/silverstripe-errorpage/commit/ab259af0707518f94561909c989617e155fd3b1b) Move phpcs to composer dependency, update Travis for it, add 7.2 to Travis (Robbie Averill)
* 2018-10-15 [ab0d7d9](https://github.com/silverstripe/silverstripe-versioned/commit/ab0d7d9e8c18b0e5ae6ee3e352317bbe0c70de53) Fix codesniffer runs in Travis (Robbie Averill)
* 2018-10-14 [c0c446a](https://github.com/silverstripe/silverstripe-versioned/commit/c0c446ad8f29dd66398feb38f5d92fa4f60a4a8b) Fix relations between staged/unstaged objects (Harsh Chokshi)
* 2018-10-11 [0aa2d66](https://github.com/silverstripe/silverstripe-admin/commit/0aa2d6615b7f207791716b6e8654d16940597be4) Use correct lazy loadable class names for GridFieldLazyLoader (Robbie Averill)
* 2018-10-11 [ee21c4201](https://github.com/silverstripe/silverstripe-framework/commit/ee21c42011fd40b2065bb2acb868a427e2232d0a) Re-instate missing SS_DATABASE_SUFFIX functionality (fixes #7966) (Loz Calver)
* 2018-10-11 [4702a22](https://github.com/silverstripe/silverstripe-admin/commit/4702a223ed2c85cd8a55501351526648a70c41b7) Defensively programming some possible failure points (Guy Marriott)
* 2018-10-11 [0db2f84ad](https://github.com/silverstripe/silverstripe-framework/commit/0db2f84ade9b1e8e2811cd7c32bf5f3510544c74) Persist TinyMCE updates when writing with Behat (Guy Marriott)
* 2018-10-10 [e941a56](https://github.com/silverstripe/silverstripe-admin/commit/e941a56b9e918908f16712f2e7be1b43d5810062) Changing the value of a TinyMCE field will correctly trigger a change in the React component (Guy Marriott)
* 2018-10-09 [f710c5c](https://github.com/silverstripe/silverstripe-admin/commit/f710c5cdcd2cf95fdaa738f55c0f2529fcbe826d) Only hide overflow from inactive chosen fields (Robbie Averill)
* 2018-10-09 [56d562193](https://github.com/silverstripe/silverstripe-framework/commit/56d56219345b4a8ba318261af98bcd62f3ce060d) Flush extra_methods statics between test runs (Robbie Averill)
* 2018-10-09 [d1281a571](https://github.com/silverstripe/silverstripe-framework/commit/d1281a571a56dca9d40b59a7baf31d32b09a37f5) Escape HTML in PHPDoc to fix API docs from rendering incorrectly (Robbie Averill)
* 2018-10-09 [522b288](https://github.com/silverstripe/silverstripe-admin/commit/522b28890e6b11b3a324a38c65199f96f86c4b2f) ModelAdmin pagination with a filter (Luke Edwards)
* 2018-10-08 [4766cae](https://github.com/silverstripe/silverstripe-admin/commit/4766cae7918e75c3b47d69487fecdb69b2993077) Retain polyfill for display block style in .collapse.show with Bootstrap 4.1.x (Robbie Averill)
* 2018-10-08 [6e649b57](https://github.com/silverstripe/silverstripe-cms/commit/6e649b570d70d83729527ea8fbbc069426f11338) CMSMain::duplicate() now checks canCreate() but not canEdit() (Robbie Averill)
* 2018-10-08 [c4788803e](https://github.com/silverstripe/silverstripe-framework/commit/c4788803ee7b903bc45541ccc0ef8446cf99922f) Remove unused cacheData prop from #8451 (Robbie Averill)
* 2018-10-08 [884a12c](https://github.com/silverstripe/silverstripe-admin/commit/884a12c864dd856a4beb8e636eb56c46be2dfa2e) Add fix for potential tabnabbing on community help links (Sacha Judd)
* 2018-10-08 [979dd38](https://github.com/silverstripe/silverstripe-assets/commit/979dd385947900b9df48928ad7ba4c2eb7a1361f) Fix migrating files with an incorrect class (Luke Edwards)
* 2018-10-08 [fdb53311b](https://github.com/silverstripe/silverstripe-framework/commit/fdb53311bac68bcee5f6b026c0f526c98ea1da65) Fix linting issue. (Maxime Rainville)
* 2018-10-08 [e06bb05](https://github.com/silverstripe/silverstripe-admin/commit/e06bb051d29c9aceb0d6863637bf038ae0715777) Ensure TinyMCE field changes are persisted before updating redux state (Guy Marriott)
* 2018-10-06 [8c7459a70](https://github.com/silverstripe/silverstripe-framework/commit/8c7459a7082ab3880202a3541bd11ed183465ef1) Fix CompositeField test that relied on a DropdownField bug (Sam Minnee)
* 2018-10-05 [e5d3b28a4](https://github.com/silverstripe/silverstripe-framework/commit/e5d3b28a4d10cb4d960897d37071246532ab8ebc) Dont break validation on selects without a source. (Sam Minnee)
* 2018-10-05 [98568262f](https://github.com/silverstripe/silverstripe-framework/commit/98568262f2c5d7cc9a9cd39af158d5df7dce12a7) Fixed phpcs violations (Robbie Averill)
* 2018-10-04 [fafd9dad6](https://github.com/silverstripe/silverstripe-framework/commit/fafd9dad6d60731c0ed6695a2df5535ea433632e) fixing name of constant ASSETS_PATH (Philipp Staender)
* 2018-10-04 [0fc06e51e](https://github.com/silverstripe/silverstripe-framework/commit/0fc06e51e5020b8959310682bade02f97653dc73) Drop seconds from DBDatetime::Nice() to restore SS3 behaviour. (Sam Minnee)
* 2018-10-03 [19af1ac](https://github.com/silverstripe/silverstripe-graphql/commit/19af1ac6d77089a5365ed9f1892306fdd943a2ca) Add codesniffer as a dev dependency and use it in Travis (Robbie Averill)
* 2018-10-03 [4668fab](https://github.com/silverstripe/silverstripe-assets/commit/4668fabc3a02d996a0a9be13245cf3ab3fba1079) Shortcode provider does not always request a protected asset grant, add tests for FlysystemAssetStore (Robbie Averill)
* 2018-10-03 [ce9496d](https://github.com/silverstripe/silverstripe-admin/commit/ce9496d2b9bcda50a4e74c386d31bac7c4dc0939) Quote injector alias references, deprecated and removed support for in Symfony 4 (Robbie Averill)
* 2018-10-03 [d535e71](https://github.com/silverstripe/silverstripe-graphql/commit/d535e71ef81165ac6e02c8b27b1e577b3a291b65) Quote injector alias references, deprecated and removed support for in Symfony 4 (Robbie Averill)
* 2018-10-03 [4740346ed](https://github.com/silverstripe/silverstripe-framework/commit/4740346ed8766549f0f948a4396954227f2494bb) Make ArrayList::limit() consistent with DataList::limit() (Sam Minnee)
* 2018-10-03 [0cc72c91a](https://github.com/silverstripe/silverstripe-framework/commit/0cc72c91ada58d6927dab6e93bfe785b623f3e7a) Use DELETE FROM instead of TRUNCATE for clearTable (Sam Minnee)
* 2018-10-02 [5970fc241](https://github.com/silverstripe/silverstripe-framework/commit/5970fc2417bcd29cfc85c209117b7ed6625141ad) Moving test to correct director (Guy Marriott)
* 2018-10-02 [79c2b5ad4](https://github.com/silverstripe/silverstripe-framework/commit/79c2b5ad427f4e95c8fb51b46c4ba31cdf2997c1) Use DELETE FROM instead of TRUNCATE for clearTable (Sam Minnee)
* 2018-10-01 [f2cbc1dfb](https://github.com/silverstripe/silverstripe-framework/commit/f2cbc1dfbb8b1972eeb72d230f7b5cc2ebad26ee) Dont use USE_FRM in MySQL repair. Fixes #6300. (Sam Minnee)
* 2018-10-01 [638e6ec28](https://github.com/silverstripe/silverstripe-framework/commit/638e6ec2814b4b4cbabd0adc0e166c4812b94740) Throw deprecation notice on limit=0 (Sam Minnee)
* 2018-10-01 [ad87890b2](https://github.com/silverstripe/silverstripe-framework/commit/ad87890b2e92f3f4092bbf9a70ab0d439d40ce31) Dont change state in ArrayList::getIterator() (Sam Minnee)
* 2018-10-01 [63cabc7](https://github.com/silverstripe/silverstripe-assets/commit/63cabc7fc84f295a95e88f2ce37f940b61b97223) Keep folder Name and Title in sync on update (Luke Edwards)
* 2018-10-01 [5c7b0da](https://github.com/silverstripe/silverstripe-admin/commit/5c7b0da18d894d32f3884fcd2f4e18e8ccd7b629) Searching now allows + symbols, use own method over jQuery serialisation (Robbie Averill)
* 2018-10-01 [71dad5f68](https://github.com/silverstripe/silverstripe-framework/commit/71dad5f68518b9052b657c8dc70d4581fb771e98) Append any fields that dont match name in insertBefore/insertAfter (Sam Minnee)
* 2018-10-01 [b0c4c5a1](https://github.com/silverstripe/silverstripe-cms/commit/b0c4c5a1775c95e1abd878f233e78b009f5d01ec) Updating SiteTree search fields to work with new search namespacing (Guy Marriott)
* 2018-10-01 [81292c5](https://github.com/silverstripe/silverstripe-asset-admin/commit/81292c52f04690349ea8d3634398faeda2190f8d) Fix outdated data in Apollo GraphQL cache when deleting/moving files (bergice)
* 2018-10-01 [5422e28](https://github.com/silverstripe/silverstripe-asset-admin/commit/5422e28635cec8f285eb422fa85f57f4418c09b8) Folder sort incorrect (Luke Edwards)
* 2018-09-28 [231d6d9a9](https://github.com/silverstripe/silverstripe-framework/commit/231d6d9a9f388e10cf77149aec22e947db648644) New members now receive the configured default locale, not the current locale (Robbie Averill)
* 2018-09-28 [ac1fe5e9d](https://github.com/silverstripe/silverstripe-framework/commit/ac1fe5e9d5de92dbdd7c03c187471fe6b5d8d7c0) joinClass's default_sort is used when nothing else has been set already (Robbie Averill)
* 2018-09-27 [fa4e031](https://github.com/silverstripe/silverstripe-admin/commit/fa4e031ef961215653e315ea441059c6945e5e3b) Update field names in Behat tests for new namespaces (Robbie Averill)
* 2018-09-27 [44b92c90](https://github.com/silverstripe/silverstripe-cms/commit/44b92c90bc18e629a73584a2a2eb0db8a02d740a) Update field names in Behat tests for new search form namespacing (Robbie Averill)
* 2018-09-27 [c54e7317d](https://github.com/silverstripe/silverstripe-framework/commit/c54e7317d2016727b1e2083996fc925fe862e9ab) Avoid having search fields with the same names as form elements (Guy Marriott)
* 2018-09-27 [2e41ea8](https://github.com/silverstripe/silverstripe-admin/commit/2e41ea83b95509ba1f68cf895c49fd846ec15841) Avoid having search fields with the same name as form elements (Guy Marriott)
* 2018-09-25 [dc59bd8](https://github.com/silverstripe/silverstripe-versioned/commit/dc59bd8e5613442b214f951ca16ec376e1ee1cda) Published GraphQL field now correctly indicates whether the record's version is published (Robbie Averill)
* 2018-09-25 [05b372c](https://github.com/silverstripe/silverstripe-versioned/commit/05b372c85f6720c931ac5dceba5fd05a84c74482) Use Hierarchy::prepopulateTreeDataCache() in CMS. (#183) (Sam Minnée)
* 2018-09-25 [5bfc37ff](https://github.com/silverstripe/silverstripe-cms/commit/5bfc37ff4bd22e8bbc02dc4f6dae59d25a4d5e67) Use Hierarchy::prepopulateTreeDataCache() in CMS (#2266) (Sam Minnée)
* 2018-09-24 [0276f6c08](https://github.com/silverstripe/silverstripe-framework/commit/0276f6c089ff5557a36eaf7367c8fc75fc6af20c) Revert semver break in adding GridField type hint to method signature (Robbie Averill)
* 2018-09-24 [f76fb26](https://github.com/silverstripe/silverstripe-asset-admin/commit/f76fb269b3450077623045ce84726dfd60f92894) fix psr (Thomas Portelange)
* 2018-09-24 [5e069ec](https://github.com/silverstripe/silverstripe-versioned/commit/5e069ec85c3b3cb74054b5cc18012531cfe22ce6) fix inferReciprocalComponent called on unsaved (Thomas Portelange)
* 2018-09-24 [9b5425d](https://github.com/silverstripe/silverstripe-graphql/commit/9b5425d5ba8a25eb799743e62733c57eb2837175) Incorrect parameter order of (Guy Marriott)
* 2018-09-24 [a2bb70c46](https://github.com/silverstripe/silverstripe-framework/commit/a2bb70c46dec00a6c9164bcc134e3fdc64a452e9) Don't flush manifests in tests by default (Ingo Schommer)
* 2018-09-21 [1d5ecd342](https://github.com/silverstripe/silverstripe-framework/commit/1d5ecd342e417b4707a3bbc34e97949bffd14afb) Prevent error on valid response status codes (Damian Mooyman)
* 2018-09-20 [9a89aad](https://github.com/silverstripe/silverstripe-admin/commit/9a89aad5df0d5c67a5575a7e20d723cf9d6c4d95) Whitelist nonce parameters from JS resources to be loaded. (Luke Edwards)
* 2018-09-20 [16b3d18](https://github.com/silverstripe/silverstripe-assets/commit/16b3d18ebdf8896ed216faf2a33daa729c6b2c09) FlysystemAssetStore::getAsURL() only grant for protected filesystems (Christopher Darling)
* 2018-09-20 [a9b2443](https://github.com/silverstripe/silverstripe-admin/commit/a9b244349435028c7b55b30475a9fe4d50207fc1) Revert changes to default dropdownToggleClassNames on ActionMenu (Sacha Judd)
* 2018-09-19 [b98c87a6c](https://github.com/silverstripe/silverstripe-framework/commit/b98c87a6c51baa6696ef9f077775f633c4c5ecd4) Ensure existing session can be accessed if headers_sent() (Sam Minnee)
* 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville)
* 2018-09-18 [db63f55fb](https://github.com/silverstripe/silverstripe-framework/commit/db63f55fbb8e635e4e7215b7b7eff4e1f1cb7b22) Changes being detected on TreeMulti as values not sorted (Luke Edwards)
* 2018-09-17 [d597166](https://github.com/silverstripe/silverstripe-versioned/commit/d5971661cec5fdf12dbfe895fad08ce6bbb05e25) Performance optimisation for draft pages in treeview (Sam Minnee)
* 2018-09-14 [0bab772](https://github.com/silverstripe/silverstripe-versioned-admin/commit/0bab77230efff516d109177ec05b22aec45c8c35) Remove apollo and graphql requirements from module, not implemented (Robbie Averill)
* 2018-09-14 [ac1de94](https://github.com/silverstripe/silverstripe-versioned-admin/commit/ac1de94890e3226ced8019609a48dc22d785b0ff) Versioned-admin now declares its dependency on silverstripe-graphql (Robbie Averill)
* 2018-09-13 [5c102dec](https://github.com/silverstripe/silverstripe-cms/commit/5c102decbde43395e14aeff83a20c4c6f1d048ae) Improve performance of CMSMain::getArchiveWarningMessage (#2231) (Maxime Rainville)
* 2018-09-12 [41c0b8fb](https://github.com/silverstripe/silverstripe-cms/commit/41c0b8fb85b7ac11f18eb1813f9e063e13cbafa2) Fix 'Insert links into a page' test (Luke Edwards)
* 2018-09-10 [fb0d81d](https://github.com/silverstripe/silverstripe-admin/commit/fb0d81d6c02bff292e69a0dcd42e3e01be728c01) Remove action menu toggle styles (Sacha Judd)
* 2018-09-10 [8ae0ef0](https://github.com/silverstripe/silverstripe-versioned/commit/8ae0ef0002a229d233f7395cfed15c979c3f1698) Do not update LeftAndMain link with Stage param (#173) (Maxime Rainville)
* 2018-09-04 [fbd8843](https://github.com/silverstripe/silverstripe-assets/commit/fbd88434cfc89eac7d75e34cdcc48f97821198ff) Remove unnecessary UploadTest\Validator (Sam Minnee)
* 2018-09-04 [40c7a0a](https://github.com/silverstripe/silverstripe-assets/commit/40c7a0aac6390237515cd30d9b23de8e7ad0f5ba) Better error message for invalid upload (Sam Minnee)
* 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) fix description for docs.silverstripe.org (wernerkrauss)
* 2018-09-03 [b922c0d73](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill)
* 2018-09-03 [1dd4c7b](https://github.com/silverstripe/silverstripe-versioned-admin/commit/1dd4c7b052d20170c6b5624083ca61f070ee2c49) Text collector translations now compile without errors (Robbie Averill)
* 2018-09-03 [641208dc](https://github.com/silverstripe/silverstripe-siteconfig/commit/641208dcd29a7afaf70c3100a1b02a0bd149b667) Text collector translations now compile without errors (Robbie Averill)
* 2018-09-03 [225445931](https://github.com/silverstripe/silverstripe-framework/commit/22544593101ce670a809f3b354f5ff850840006b) Text collector translations now compile without errors (Robbie Averill)
* 2018-08-31 [f5869a5](https://github.com/silverstripe/silverstripe-campaign-admin/commit/f5869a56ba1c34568d424a2cf71000abfa0ef206) Do not render view mode toggle on campaign toolbar if the campaign is empty (bergice)
* 2018-08-31 [68c2c976d](https://github.com/silverstripe/silverstripe-framework/commit/68c2c976d4813607a420ac4cda7b01f0a7aee8c7) Fix alignment test step definition (#8354) (Luke Edwards)
* 2018-08-30 [234b795f8](https://github.com/silverstripe/silverstripe-framework/commit/234b795f89657c6b25da6101a9fc878e3297c301) Use classes for TinyMCE alignment buttons (Luke Edwards)
* 2018-08-30 [5488b31](https://github.com/silverstripe/silverstripe-admin/commit/5488b31f84f47a18d40cf34d7da53d498b169496) Add explicit `0` z-index to `cms-content` so the menu toggle can render above it (#620) (Andre Kiste)
* 2018-08-30 [463fdef](https://github.com/silverstripe/silverstripe-admin/commit/463fdefde0f827d783f9a519f261d1e4d35c01ab) Remove "more" action icon size, add btn-sm and fix icon alignment in gridfield (Sacha Judd)
* 2018-08-28 [dbfc25302](https://github.com/silverstripe/silverstripe-framework/commit/dbfc253021bce3997af0934b9015215047bbac7b) Fix incorrect version number in 4.3.0 changelog (Loz Calver)
* 2018-08-28 [d1951c94](https://github.com/silverstripe/silverstripe-cms/commit/d1951c946fe143e79ea6a7e1ee55ae90586c8a33) Sort history viewer versions in descending order (Robbie Averill)
* 2018-08-28 [10ef38f](https://github.com/silverstripe/silverstripe-campaign-admin/commit/10ef38f039fa4bf29be764ffd99196b0f9b62554) Hide 1px left border in preview component if we are in 'Preview Only' mode (bergice)
* 2018-08-28 [d651d0fbf](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill)
* 2018-08-27 [2ab622f](https://github.com/silverstripe/silverstripe-admin/commit/2ab622f88a5a179a7186b46699055cf761d3b749) Fix Add mock store to the loadComponent AppolloProvider (Maxime Rainville)
* 2018-08-27 [4da556923](https://github.com/silverstripe/silverstripe-framework/commit/4da5569232505ee574e0b5106ff2116611393aa4) ensure createFromVariables takes correct params on CLIRequestBuilder (Scott Hutchinson)
* 2018-08-27 [f3230c78](https://github.com/silverstripe/silverstripe-reports/commit/f3230c78d4e3731a10a5f4c508bc68c6a8534866) Use requestVar() to include post vars as well as get vars (Robbie Averill)
* 2018-08-24 [e196475](https://github.com/silverstripe/silverstripe-assets/commit/e196475220e1b97cc61f8e026b55984d2e240e0d) Graceful validation of image shortcode (Aaron Carlino)
* 2018-08-24 [9b7b476](https://github.com/silverstripe/silverstripe-versioned-admin/commit/9b7b47689558f184ed7b448fd81ef80ed0c47926) Label RestoreAction clearer as 'Restore *to* draft' (#42) (Luke Edwards)
* 2018-08-24 [2b16e2a](https://github.com/silverstripe/silverstripe-admin/commit/2b16e2afbaa322ef4ab2fce6c2add9b8f5596ba4) GridField delete button to offer archive action if possible (#602) (Luke Edwards)
* 2018-08-24 [6164d01d6](https://github.com/silverstripe/silverstripe-framework/commit/6164d01d65648ce6b25a7ef82fabaa10b81565d0) GridField delete button to offer archive action if possible (#8325) (Luke Edwards)
* 2018-08-23 [f37dd74](https://github.com/silverstripe/silverstripe-admin/commit/f37dd74be7afae5e40e85ce2a90a4d92bf7e80bb) Site tree items do not disappear on save with source file comments enabled (Robbie Averill)
* 2018-08-23 [d6b1d31](https://github.com/silverstripe/silverstripe-versioned-admin/commit/d6b1d31f7aee8a57d97d387870b7e7c02ba98f58) Check ID is numeric before using it (Robbie Averill)
* 2018-08-22 [1b67bb08c](https://github.com/silverstripe/silverstripe-framework/commit/1b67bb08c8b61ad7e5324ef07eaea2834772b818) Fix failing HTML button test step (Luke Edwards)
* 2018-08-21 [6be253c](https://github.com/silverstripe/silverstripe-versioned-admin/commit/6be253c1b59fd04443591912abeb2dfe8d3dbc2a) Use get_one_by_stage when fetching SiteTree for a specific stage (Guy Marriott)
* 2018-08-21 [7d18f3a](https://github.com/silverstripe/silverstripe-versioned-admin/commit/7d18f3a5ec17a8893d346bc0743071c38d15146c) Disable compare mode when there is only one version available (Robbie Averill)
* 2018-08-20 [6684fc2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/6684fc2d829ea966b36ada7bc5f2cace7a59e7e8) Center alert message and conditionally show close button/selected message for IE (Raissa North)
* 2018-08-20 [dbab69669](https://github.com/silverstripe/silverstripe-framework/commit/dbab6966908f0a293ee6d469cec6b4650dc5a0f1) Message when changing password with invalid token now contains correct links to login (Robbie Averill)
* 2018-08-20 [9da7f99](https://github.com/silverstripe/silverstripe-versioned/commit/9da7f991f33ac16070b2e47b764b216a87f96622) Draft content requiring login message now correctly renders HTML link (Robbie Averill)
* 2018-08-17 [160d595e2](https://github.com/silverstripe/silverstripe-framework/commit/160d595e226edcbaa64a47a0be74193a8b8058cc) fix trailing whitespace (maks)
* 2018-08-17 [16217f365](https://github.com/silverstripe/silverstripe-framework/commit/16217f3655c28ddcf6a721bca82d45d65b91e3ed) fix accidentaly deleted comma (maks)
* 2018-08-17 [c361b09](https://github.com/silverstripe/silverstripe-admin/commit/c361b091b1640c25f1d23914489212fce1e29377) overflow of chosen dropdowns when inactive (Scott Hutchinson)
* 2018-08-16 [81e9c0c](https://github.com/silverstripe/silverstripe-versioned-admin/commit/81e9c0ce3bfad85a9e78d18b4c0d3fc4a245eab2) When exiting compare mode, the version shown is the last selected version to compare from (Robbie Averill)
* 2018-08-16 [61c046c](https://github.com/silverstripe/silverstripe-versioned/commit/61c046c9cd6ce97456b6123a05438c7cd05d07cc) If archive's possible switch GridField delete button with archive (Luke Edwards)
* 2018-08-16 [66cd3af](https://github.com/silverstripe/silverstripe-admin/commit/66cd3af09fcf68bf177a46ac57434442642d1b7c) Filtering or paginating a gridfield causing a change event (Luke Edwards)
* 2018-08-15 [726c464](https://github.com/silverstripe/silverstripe-versioned-admin/commit/726c464461fc3291ef004408d39bc77e8c8f3367) Don't indicate pointer cursor when hovering a selected version in the list (Robbie Averill)
* 2018-08-15 [7d94029](https://github.com/silverstripe/silverstripe-versioned-admin/commit/7d94029547ce8857729967153a3e6c85391e7c54) Inconsistent background and border colours in compare mode notice (Robbie Averill)
* 2018-08-15 [c189f14](https://github.com/silverstripe/silverstripe-versioned-admin/commit/c189f14322f8980f4d3218a64439db916ae32dfe) Reduce size of border radius on compare mode notice close button (Robbie Averill)
* 2018-08-15 [d9154bffb](https://github.com/silverstripe/silverstripe-framework/commit/d9154bffbf7b0031e5bd3ed1f68db3fae6ab5959) text/json is not a valid mimetype (Daniel Hensby)
* 2018-08-15 [d18b5ee](https://github.com/silverstripe/silverstripe-campaign-admin/commit/d18b5eed63e081a4cbcbb30edcf51839a2ae3461) text/json is not a valid mimetype (Daniel Hensby)
* 2018-08-15 [41a2a0c](https://github.com/silverstripe/silverstripe-admin/commit/41a2a0c38c073d82b96fd4fa2fa09bea3b556aa5) text/json is not a mimetype (Daniel Hensby)
* 2018-08-15 [0db594b2d](https://github.com/silverstripe/silverstripe-framework/commit/0db594b2d39c93dd2e911414bee5520c84048906) Remove double escaping of HTML values in print views (Robbie Averill)
* 2018-08-15 [0c713b5](https://github.com/silverstripe/silverstripe-assets/commit/0c713b5b1eb6a08ac00dcadb187b8b3ef7115fc4) Fix routing for files with dots in filename (Damian Mooyman)
* 2018-08-14 [fcaa9ba](https://github.com/silverstripe/silverstripe-versioned/commit/fcaa9ba7a68a839d84ff23d32275c510d0f9890e) Restore and archive action improvements (Luke Edwards)
* 2018-08-14 [fc7f712](https://github.com/silverstripe/silverstripe-admin/commit/fc7f7120a67ce95b03f13b4c8fb90b36f810f7b1) Modal response animation appearing outside the modal (#601) (Luke Edwards)
* 2018-08-14 [873873dc3](https://github.com/silverstripe/silverstripe-framework/commit/873873dc303ce2041aa23e365464133a359e1561) Pass request to dummy controller before calling init (Robbie Averill)
* 2018-08-14 [27ac001d5](https://github.com/silverstripe/silverstripe-framework/commit/27ac001d5b27cce4f80ce4b3335c14708b116830) email rendering should not include requirements (Thomas Portelange)
* 2018-08-14 [8ec551e5](https://github.com/silverstripe/silverstripe-cms/commit/8ec551e57b04d00d6897d06c2779557f0ec8109d) Broken "show as list" (#2232) (Maxime Rainville)
* 2018-08-12 [9f5b0086c](https://github.com/silverstripe/silverstripe-framework/commit/9f5b0086cb1a0259c5c87ea205390c5e69dcae90) Paginating a gridfield causing a change event (Luke Edwards)
* 2018-08-10 [8611e47](https://github.com/silverstripe/silverstripe-versioned-admin/commit/8611e472f62cc7f94fed1e6a9b1239a76cdf008a) Allow more width for version numbers in list, so alignment of state column is unaffected (Raissa North)
* 2018-08-10 [d4995f52](https://github.com/silverstripe/silverstripe-cms/commit/d4995f5204f020f75fbddb3e49b944a54be5c6c2) Separating ModelAsController catch-all route to apply after all other configuration (Guy Marriott)
* 2018-08-08 [5596ae2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/5596ae2ff1b8ee99fa0667b8f0caa7007debb629) Remove new gridfield search, data query needs to be altered to work (Luke Edwards)
* 2018-08-08 [eed1ca9](https://github.com/silverstripe/silverstripe-versioned-admin/commit/eed1ca93d3da1f74c06995bb603e062c4042aaab) Change list item anchor for span with role="button" to justify using tabIndex="0" (Robbie Averill)
* 2018-08-08 [e14ab99](https://github.com/silverstripe/silverstripe-graphql/commit/e14ab991f5c99cee6b1bdfa18ab07a1e4b40961e) Don't rely on return value of GraphQL scaffolding providers (#171) (Guy Marriott)
* 2018-08-08 [aed0726](https://github.com/silverstripe/silverstripe-versioned-admin/commit/aed072680836a30ea482faf1aa8c72b93a7a5eda) Add roles to list and list items, make list items keyboard accessible (Robbie Averill)
* 2018-08-07 [c2b54c7](https://github.com/silverstripe/silverstripe-admin/commit/c2b54c72a990df9d453bedc307f613f23107bfad) graphql route getting overwritten (Aaron Carlino)
* 2018-08-06 [df7396e8](https://github.com/silverstripe/silverstripe-cms/commit/df7396e8845eea7a75e73237de9ee7e4cb6568f6) CMS routes are now run after #coreroutes without re-including itself (Robbie Averill)
* 2018-08-06 [2614804](https://github.com/silverstripe/silverstripe-versioned-admin/commit/26148046d967ddc80df76b4e280731ed338bb8df) getLatestVersion now looks at the LatestDraftVersion version property (Robbie Averill)
* 2018-08-06 [e7cb0156](https://github.com/silverstripe/silverstripe-cms/commit/e7cb0156c69a3701b248dbbae4e72f8c0b372efd) Use LatestDraftVersion in GraphQL query to determine latest draft version (Robbie Averill)
* 2018-08-06 [13372f9a3](https://github.com/silverstripe/silverstripe-framework/commit/13372f9a37d1cb19f658404c79c2be6fbfa557b1) Installer redirect to home/ (without domain) (Michael Strong)
* 2018-08-06 [855e1f2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/855e1f2ec7eb03674d3a3d3f4b051ae86924d949) Margins are correct when both in and out of a GridField, and colours used in compare warning (Robbie Averill)
* 2018-08-02 [24927c5](https://github.com/silverstripe/silverstripe-campaign-admin/commit/24927c5aa18f9adc8ef79f0adf879f6bcd5c130c) Ensure only toolbar buttons that are immediate descendants of toolbars are given margins (Robbie Averill)
* 2018-08-01 [a981584](https://github.com/silverstripe/silverstripe-admin/commit/a9815845c0e923587fa81bdbac77be43f6d4dd1a) Remove rogue CSS margin on toolbar buttons. Implemented in campaign-admin preview instead. (Robbie Averill)
* 2018-08-01 [405d8a3](https://github.com/silverstripe/silverstripe-campaign-admin/commit/405d8a3213852f4a35e7cf8101df72c206d1b2f9) Toolbar button margins are constrained to campaign previews, and update ViewModeActions name (Robbie Averill)
* 2018-08-01 [6889a1a](https://github.com/silverstripe/silverstripe-admin/commit/6889a1adf0005e91288c4e2ddc6a2f3ea1b6b593) ViewModeToggle now uses BEM class naming convention (Robbie Averill)
* 2018-08-01 [58d128e](https://github.com/silverstripe/silverstripe-versioned-admin/commit/58d128ebe02223a4b9412673140441a5e3ec7a5f) Update case on viewModeActions import and recompile (Robbie Averill)
* 2018-08-01 [8e611ac](https://github.com/silverstripe/silverstripe-versioned-admin/commit/8e611acd818aea731548edb9dcaee19ab2920e9b) Convert compareType to use version objects, restructure tests and use variables for colours (Robbie Averill)
* 2018-08-01 [763138f](https://github.com/silverstripe/silverstripe-versioned-admin/commit/763138fdd4eb2bb84019b437cb7f373c2fe540ca) remove double-margins from version comparison (Dylan Wagstaff)
* 2018-07-30 [2758348](https://github.com/silverstripe/silverstripe-versioned-admin/commit/2758348be0b84fdda27d0dbf5ee4078eece6929e) fix rebase (Dylan Wagstaff)
* 2018-07-30 [420c3f8](https://github.com/silverstripe/silverstripe-asset-admin/commit/420c3f807d7426af2c76d778faa4ef26ab5dda11) Editor should ignore drag-and-drop files (#814) (Luke Edwards)
* 2018-07-30 [fde7b9ddc](https://github.com/silverstripe/silverstripe-framework/commit/fde7b9ddc5da697395897249819b3c52530692b6) Specify minimum composer version (Maxime Rainville)
* 2018-07-27 [85b4b48fb](https://github.com/silverstripe/silverstripe-framework/commit/85b4b48fb5489cdba4b18cbf510d883986dd61c1) Restore default delete action on GridFieldConfig_RecordEditor (Maxime Rainville)
* 2018-07-27 [67254da1](https://github.com/silverstripe/silverstripe-reports/commit/67254da18599f0fe86921098524ec3303d9de41e) Apply missing class to report header. (Maxime Rainville)
* 2018-07-27 [0d90cdb05](https://github.com/silverstripe/silverstripe-framework/commit/0d90cdb05d058763e5e52720ab653c5cc391dc3b) Altering ID of authenticator tabs to resolve ID conflict (Guy Marriott)
* 2018-07-26 [900ca9c8d](https://github.com/silverstripe/silverstripe-framework/commit/900ca9c8d75b70b13b425365022ec3f1f0ebe461) Recommend install of upgrader with PHAR exec. (Maxime Rainville)
* 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater)
* 2018-07-25 [0035f4a90](https://github.com/silverstripe/silverstripe-framework/commit/0035f4a90728d9e109b12585b32491a2afeaa916) Fix backtick in changelog breaking sentence formatting (Michal Kleiner)
* 2018-07-24 [3e044dc](https://github.com/silverstripe/silverstripe-versioned-admin/commit/3e044dcba6c558a42b2afa4e52c484a0fbb7e2a2) History viewer list is now rendered as an unordered list instead of a table (Robbie Averill)
* 2018-07-24 [6c421b5](https://github.com/silverstripe/silverstripe-versioned-admin/commit/6c421b5ef15f2ffd21ec8d36ebbc7de2cb034ea0) Badge margin is moved to the text to allow it to break lines nicely on mobile (Robbie Averill)
* 2018-07-23 [a0487e5](https://github.com/silverstripe/silverstripe-admin/commit/a0487e59fc04af0d15e66d4c2874051288b4e63e) Treat readonly as disabled and fix handling for ui-constructive class (Robbie Averill)
* 2018-07-23 [55dc009](https://github.com/silverstripe/silverstripe-versioned-admin/commit/55dc0095b992cae9500109a94f0050bf797b7099) SET_COMPARE_MODE reducer no longer clears previously set compareFrom value (Robbie Averill)
* 2018-07-23 [35afd7f](https://github.com/silverstripe/silverstripe-versioned-admin/commit/35afd7fbb2f0bd330fc68fc9b52197ccd024be63) Padding and scrolling around detail views is now consistent (Robbie Averill)
* 2018-07-23 [9ac05c7](https://github.com/silverstripe/silverstripe-versioned-admin/commit/9ac05c7c8fd2438ebb9f8a3941197c46cd46ed65) Set min-width for compare enabled table to 100% to prevent margin overflow (Robbie Averill)
* 2018-07-23 [9d26ba0](https://github.com/silverstripe/silverstripe-versioned-admin/commit/9d26ba0a71633b0afc455563eac19137579186b9) Fix margins when choosing versions to compare (#11) (Raissa North)
* 2018-07-23 [92eab59](https://github.com/silverstripe/silverstripe-versioned-admin/commit/92eab5969d664531efb909ffee54476fb052c4f7) selecting two rows in list view UI issues (Guy Marriott)
* 2018-07-16 [e1296d48](https://github.com/silverstripe/silverstripe-reports/commit/e1296d4813ac1b677aa7a612ba0ad3b2ba62ccae) Filter var can be returned correctly from get variables as a fallback (Robbie Averill)
* 2018-07-13 [d1024ee](https://github.com/silverstripe/silverstripe-assets/commit/d1024ee00b12c3a212fe12d168f4521e2188274b) Add HTMLFragment casting to $Tag (#148) (Jake Bentvelzen)
* 2018-07-12 [a8e5616](https://github.com/silverstripe/silverstripe-admin/commit/a8e56166f01ef0fb8ecd46edb4eddde30447cdb0) Update GridField.js so it works with new Archive View (#559) (Luke Edwards)
* 2018-07-12 [599a4420b](https://github.com/silverstripe/silverstripe-framework/commit/599a4420bf0d982343faa6145afaf6592566bb40) Improve GridFieldViewButton to work with new Archive Admin (#8240) (Luke Edwards)
* 2018-07-11 [c2347310](https://github.com/silverstripe/silverstripe-cms/commit/c23473103e4f93b82d60f1260923c9e413d02c41) URLSegment field styling fixes #2193 (Maxime Rainville)
* 2018-07-05 [91068c23b](https://github.com/silverstripe/silverstripe-framework/commit/91068c23b5cb448fe63ae9f40875a8f0818dbe1f) Make column query not distinct (Al Twohill)
* 2018-07-05 [730fc42](https://github.com/silverstripe/silverstripe-admin/commit/730fc42ef3e6d25345516f1583d05bf968bf762c) Fix test for password recovery (Ingo Schommer)
* 2018-07-01 [3262665b2](https://github.com/silverstripe/silverstripe-framework/commit/3262665b2d6bb6d56186be2e2e370b853f13c6b5) Fix link and turn of phrase. (Maxime Rainville)
* 2018-06-29 [cc9b36e01](https://github.com/silverstripe/silverstripe-framework/commit/cc9b36e01124349ea7ccbd0902d1b01c764f82f7) fix link (Lukas)
* 2018-06-27 [8ccebf8](https://github.com/silverstripe/silverstripe-admin/commit/8ccebf813e95980363a92ec37332d2241327441f) Stop sslink from hijacking anchor plugin (Will Rossiter)
* 2018-06-22 [f9de357](https://github.com/silverstripe/silverstripe-admin/commit/f9de35724c4fb75c8a6d38e4f5b9185531fc961c) - Grid field headers misaligned (Petar Simic)
* 2018-06-18 [2f1c2992f](https://github.com/silverstripe/silverstripe-framework/commit/2f1c2992f8f61b4a87a0a363db289a19ac5a821b) Default cache state should be `no-cache` (Daniel Hensby)
* 2018-06-17 [25b0a18](https://github.com/silverstripe/silverstripe-admin/commit/25b0a18a743e81951a2d2df387b6e5442d0253c3) Fix display of GridField link existing button (Luke Edwards)
* 2018-05-18 [953153500](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill)
* 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver)

View File

@ -57,6 +57,5 @@ read our guide on [how to write secure code](/developer_guides/security/secure_c
## Sharing your Opinion
* [silverstripe.org/forums](http://www.silverstripe.org/community/forums/): Forums on silverstripe.org
* [silverstripe-dev](http://groups.google.com/group/silverstripe-dev/): Core development mailinglist
* [forum.silverstripe.org](http://forum.silverstripe.org): Forums on silverstripe.org
* [All issues across modules](https://www.silverstripe.org/community/contributing-to-silverstripe/github-all-core-issues)

View File

@ -93,7 +93,7 @@ Once your pull request is issued, it's not the end of the road. A [core committe
If you've been naughty and not adhered to the [coding conventions](coding_conventions),
expect a few requests to make changes so your code is in-line.
If your change is particularly significant, it may be referred to the [mailing list](https://groups.google.com/forum/#!forum/silverstripe-dev) for further community discussion.
If your change is particularly significant, it may be referred to the [forum](https://forum.silverstripe.org) for further community discussion.
A core committer will also "label" your PR using the labels defined in GitHub, these are to correctly classify and help find your work at a later date.
@ -165,7 +165,7 @@ After you have edited the file, GitHub will offer to create a pull request for y
[API documentation](https://api.silverstripe.org/) for good examples.
* Check and update documentation on [docs.silverstripe.org](https://docs.silverstripe.org). Check for any references to functionality deprecated or extended through your patch. Documentation changes should be included in the patch.
* When introducing something "noteworthy" (new feature, API change), [update the release changelog](/changelogs) for the next release this commit will be included in.
* If you get stuck, please post to the [forum](https://www.silverstripe.org/community/forums) or for deeper core problems, to the [core mailinglist](https://groups.google.com/forum/#!forum/silverstripe-dev)
* If you get stuck, please post to the [forum](https://www.silverstripe.org/community/forums)
* When working with the CMS, please read the ["CMS Architecture Guide"](/developer_guides/customising_the_admin_interface/cms_architecture) first
## Commit Messages

View File

@ -136,7 +136,7 @@ timeline and ask the reporter to keep the issue confidential until we announce i
Additionally, [CVE](http://cve.mitre.org) numbers are accepted.
* Halt all other development as long as is needed to develop a fix, including patches against the current and one
previous major release (if applicable).
* Pre-announce the upcoming security release to a private mailing list of important stakeholders (see below).
* Pre-announce the upcoming security release to aprivate pre-announcement mailing list of important stakeholders (see below).
* We will inform you about resolution and [announce](https://forum.silverstripe.org/c/releases) a
[new release](http://silverstripe.org/security-releases/) publically.
@ -179,7 +179,7 @@ Follow these instructions in sequence as much as possible:
* Before release (or release candidate)
* Merge back from [http://github.com/silverstripe-security](http://github.com/silverstripe-security) repos shortly at the release (minimise early disclosure through source code)
* Merge up to newer minor release branches (see [Supported Versions](#supported-versions))
* Send out a note on the pre-announce list with a highlevel description of the issue and impact (usually a copy of the yet unpublished security release page on silverstripe.org)
* Send out a note on the pre-announcement mailing list with a highlevel description of the issue and impact (usually a copy of the yet unpublished security release page on silverstripe.org)
* Link to silverstripe.org security release page in the changelog.
* Move the issue to "Awaiting Release" in the [project board](https://github.com/silverstripe-security/security-issues/projects/1)
* Perform release
@ -189,22 +189,24 @@ Follow these instructions in sequence as much as possible:
* Respond to issue reporter with reference to the release on the same discussion thread (cc security@silverstripe.org)
* Move the issue to "Done" in the [project board](https://github.com/silverstripe-security/security-issues/projects/1)
### Pre-announce Mailinglist
### Pre-announcement mailing list
In addition to our public disclosure process, we maintain a private mailinglist
where upcoming security releases will be pre-announced. Members in this list will receive a security
pre-announcement as soon as it has been sufficiently researched,
alongside a timeline for the upcoming release. This will happen a few days before
the announcement goes public alongside new release, and most likely before a patch has been developed.
In addition to our public disclosure process, we maintain a private mailing list where upcoming security releases
are pre-announced. Members of this list will receive a security pre-announcement, as soon as it has been
sufficiently researched, with a timeline for the upcoming release.
This will happen a few days before the announcement goes public alongside a new release,
and most likely before a patch has been developed.
Since well distribute sensitive info on unpatched vulnerabilities in this list,
the selection criteria for joining naturally has to be strict.
Applicants should provide references within the community,
as well as a demonstrated need for this level of information (e.g. a large website with sensitive customer data).
Since well distribute sensitive information on unpatched vulnerabilities in this list,
the selection criteria for joining naturally has to be strict.
Applicants should provide references within the community,
as well as a demonstrated need for this level of information
(e.g. involvement with a large website with sensitive customer data).
You dont need to be a client of SilverStripe Ltd to get on board,
but we will need to perform some low-touch background checks to ensure identity.
but we will need to perform some low-touch background checks to verify your identity.
Please contact security@silverstripe.org for details.
## Quality Assurance and Testing
The quality of our software is important to us, and we continously test it for regressions

View File

@ -1,5 +1,5 @@
title: Making a SilverStripe core release
summary: Development guide for core contributors to build and publish a new release
summary: Development guide for core contributors to build and publish a new release
# Making a SilverStripe core release
@ -28,8 +28,11 @@ As a core contributor it is necessary to have installed the following set of too
* [cow release tool](https://github.com/silverstripe/cow#install). This should typically
be installed in a global location via the below command. Please see the installation
docs on the cow repo for more setup details.
`composer global require silverstripe/cow dev-master`
* [transifex client](http://docs.transifex.com/client/).
`composer global require silverstripe/cow ^2`
* [satis repository tool](https://github.com/composer/satis). This should be installed
globally for minimum maintenance.
`composer global require composer/satis ^1`
* [transifex client](http://docs.transifex.com/client/).
`pip install transifex-client`
If you're on OSX 10.10+, the standard Python installer is locked down.
Use `brew install python; sudo easy_install pip` instead
@ -115,10 +118,14 @@ Producing a security fix follows this general process:
release date of the final stable is not known, then it's ok to give an estimated
release schedule.
* Push the current upstream target branches (e.g. 3.2) to the corresponding security fork
to a new branch named for the target release (e.g. 3.2.4). Security fixes should be
applied to this branch only. Once a fix (or fixes) have been applied to this branch, then
a tag can be applied, and a private release can then be developed in order
to test this release.
to the equivalent branch on [silverstripe-security](https://github.com/silverstripe-security).
Security fixes should be applied to the branch on this private repository only.
Once a fix (or fixes) have been applied to this branch, then a tag can be applied,
and a private release can then be developed in order to test this release.
* Once upstream branches are all pushed to the security forks, make sure to merge all
security fixes into those branches prior to running cow.
* Setup a temporary [satis](https://github.com/composer/satis) repository which points to all relevant repositories
containing security fixes. See below for setting up a temporary satis repository.
* Once release testing is completed and the release is ready for stabilisation, then these fixes
can then be pushed to the upstream module fork, and the release completed as per normal.
Make sure to publish any draft security pages at the same time as the release is published (same day).
@ -131,17 +138,64 @@ a public stable, not an RC or dev-branch. Security warnings that do not require
can be published as soon as a workaround or usable resolution exists.
</div>
### Setting up satis for hosting private security releases
When installing a project from protected repositories, it's necessary prior to creating your project
to override the public repository URLs with the private repositories containing undisclosed fixes. For
this we use [satis](https://github.com/composer/satis).
To setup a Satis project for a release:
* Ensure Satis is installed globally: `composer global require composer/satis ^1`
* `cd ~/Sites/` (or wherever your web-root is located)
* `mkdir satis-security && cd satis-security` (or some directory specific to your release)
* Create a config file (e.g. config.json) of the given format (add only those repositories necessary).
Note:
- The homepage path should match the eventual location of the package content
- You should add the root repository (silverstripe/installer) to ensure
`create-project` works (even if not a private security fork).
- You should add some package version constraints to prevent having to parse
all legacy tags and all branches.
```json
{
"name": "SilverStripe Security Repository",
"homepage": "http://localhost/satis-security/public",
"repositories": {
"installer": {
"type": "vcs",
"url": "https://github.com/silverstripe/silverstripe-installer.git"
},
"framework": {
"type": "vcs",
"url": "https://github.com/silverstripe-security/silverstripe-framework.git"
}
},
"require": {
"silverstripe/installer": "^3.5 || ^4",
"silverstripe/framework": "^3.5 || ^4"
},
"require-all": true
}
```
* Build the repository:
`satis build config.json ./public`
* Test you can view the satis home page at `http://localhost/satis-security/public/`
* When performing the release ensure you use `--repository=http://localhost/satis-security/public` (below)
<div class="warning" markdown="1">
It's important that you re-run `satis build` step after EVERY change that is pushed upstream; E.g. between
each release, if making multiple releases.
</div>
## Standard release process
The release process, at a high level, involves creating a release, publishing it, and
The release process, at a high level, involves creating a release, publishing it, and
reviewing the need for either another pre-release or a final stable tag within a short period
(normally within 3-5 business days).
During the pre-release cycle a temporary branch is created, and should only receive
absolutely critical fixes during the cycle. Any changes to this branch should
result in the requirement for a new release, thus a higher level of scrutiny is typically
placed on any pull request to these branches.
When creating a new pre-release or stable, the following process is broken down into two
main sets of commands:
@ -161,7 +215,7 @@ Check all tickets assigned to that milestone are either closed or reassigned to
Use the [list of all issues across modules](https://www.silverstripe.org/community/contributing-to-silverstripe/github-all-core-issues)
as a starting point, and add a `milestone:"your-milestone"` filter.
Merge up from other older [supported release branches](release-process#supported-versions) (e.g. merge `3.1`->`3.2`, `3.2`->`3.3`, `3.3`->`3`, `3`->`master`).
Merge up from other older [supported release branches](release-process#supported-versions) (e.g. merge `4.0`->`4.1`, `4.1`->`4.2`, `4.2`->`4`, `4`->`master`).
This is the part of the release that prepares and tests everything locally, but
doe not make any upstream changes (so it's safe to run without worrying about
@ -169,13 +223,30 @@ any mistakes migrating their way into the public sphere).
Invoked by running `cow release` in the format as below:
```
cow release <version> -vvv
```
`cow release <version> [recipe] -vvv`
This command has the following parameters:
E.g.
* `<version>` The version that is to be released. E.g. 3.2.4 or 4.0.0-alpha4
`cow release 4.0.1 -vvv`
* `<version>` The version that is to be released. E.g. `4.1.4` or `4.3.0-rc1`
* `<recipe>` `Optional: the recipe that is being released (default: "silverstripe/installer")
This command has these options (note that --repository option is critical for security releases):
* `-vvv` to ensure all underlying commands are echoed
* `--directory <directory>` to specify the folder to create or look for this project in. If you don't specify this,
it will install to the path specified by `./release-<version>` in the current directory.
* `--repository <repository>` will allow a custom composer package url to be specified. E.g. `http://packages.cwp.govt.nz`
See the above section "Setting up satis for hosting private security releases" on how to prepare a custom
repository for a security release.
* `--branching <type>` will specify a branching strategy. This allows these options:
* `auto` - Default option, will branch to the minor version (e.g. 1.1) unless doing a non-stable tag (e.g. rc1)
* `major` - Branch all repos to the major version (e.g. 1) unless already on a more-specific minor version.
* `minor` - Branch all repos to the minor semver branch (e.g. 1.1)
* `none` - Release from the current branch and do no branching.
* `--skip-tests` to skip tests
* `--skip-i18n` to skip updating localisations
This can take between 5-15 minutes, and will invoke the following steps,
each of which can also be run in isolation (in case the process stalls
@ -188,14 +259,17 @@ and needs to be manually advanced):
know to install dev-master, and installing 3.3.0 will install from 3.x-dev.
If installing pre-release versions for stabilisation, it will use the correct
temporary release branch.
* `release:plan` The release planning will take place, this reads the various dependencies of the recipe being released
and determines what new versions of those dependencies need to be tagged to create the final release. The conclusion
of the planning step is output to the screen and requires user confirmation.
* `release:branch` If release:create installed from a non-rc branch, it will
create the new temporary release branch (via `--branch-auto`). You can also customise this branch
with `--branch=<branchname>`, but it's best to use the standard.
* `release:translate` All upstream transifex strings will be pulled into the
local master strings, and then the [i18nTextCollector](api:SilverStripe\i18n\TextCollection\i18nTextCollector) task will be invoked
and will merge these strings together, before pushing all new master strings
back up to transifex to make them available for translation. Changes to these
files will also be automatically committed to git.
local master strings, and then the [i18nTextCollector](api:SilverStripe\i18n\TextCollection\i18nTextCollector)
task will be invoked and will merge these strings together, before pushing all
new master strings back up to transifex to make them available for translation.
Changes to these files will also be automatically committed to git.
* `release:test` Will run all unit tests on this release. Make sure that you
setup your `.env` correctly (as above) so that this will work.
* `release:changelog` Will compare the current branch head with `--from` parameter
@ -218,9 +292,7 @@ the build status of Behat end-to-end tests manually on travis-ci.org.
Check the badges on the various modules available on [github.com/silverstripe](http://github.com/silverstripe).
It's also ideal to eyeball the git changes generated by the release tool, making sure
that no translation strings were unintentionally lost, no malicious changes were
introduced in the (community contributed) translations, and that the changelog
was generated correctly.
that no translation strings were unintentionally lost, and that the changelog was generated correctly.
In particular, double check that all necessary information is included in the release notes,
including:
@ -240,14 +312,29 @@ building an archive, and uploading to
Invoked by running `cow release:publish` in the format as below:
```
cow release:publish <version> -vvv
```
`cow release:publish <version> [<recipe>] -vvv`
E.g.
`cow release:publish 4.0.1 silverstripe/installer`
This command has these options:
* `-vvv` to ensure all underlying commands are echoed
* `--directory <directory>` to specify the folder to look for the project created in the prior step. As with
above, it will be guessed if omitted. You can run this command in the `./release-<version>` directory and
omit this option.
* `--aws-profile <profile>` to specify the AWS profile name for uploading releases to s3. Check with
damian@silverstripe.com if you don't have an AWS key setup.
* `--skip-archive-upload` to disable both "archive" and "upload". This is useful if doing a private release and
you don't want to upload this file to AWS.
* `--skip-upload` to disable the "upload" command (but not archive)
As with the `cow release` command, this step is broken down into the following
subtasks which are invoked in sequence:
* `release:tag` Each module will have the appropriate tag applied (except the theme).
* `release:push` The temporary release branches and all tags are pushed up to origin on github.
* `release:tag` Each module will have the appropriate tag applied (except the theme). All tags are pushed up to origin
on github.
* `release:archive` This will generate a new tar.gz and zip archive, each for
cms and framework-only installations. These will be copied to the root folder
of the release directory, although the actual build will be created in temporary
@ -255,7 +342,7 @@ subtasks which are invoked in sequence:
If the tags generated in the prior step are not yet available on packagist (which can
take a few minutes at times) then this task will cycle through a retry-cycle,
which will re-attempt the archive creation periodically until these tags are available.
* `release:upload` This will invoke the AWS CLI command to upload these archives to the
* `release:upload` This will invoke the AWS CLI command to upload these archives to the
s3 bucket `silverstripe-ssorg-releases`. If you have setup your AWS profile
for silverstripe releases under a non-default name, you can specify this profile
on the command line with the `--aws-profile=<profile>` command.
@ -264,23 +351,23 @@ subtasks which are invoked in sequence:
Once all of these commands have completed there are a couple of final tasks left that
aren't strictly able to be automated:
* If this is a stable release, it will be necessary to perform a post-release merge
* It will be necessary to perform a post-release merge
on open source. This normally will require you to merge the temporary release branch into the
source branch (e.g. merge 3.2.4 into 3.2), or sometimes create new branches if
releasing a new minor version, and bumping up the branch-alias in composer.json.
E.g. branching 3.3 from 3, and aliasing 3 as 3.4.x-dev. You can then delete
the temporary release branches. This will need to be done before updating the
the temporary release branches. This will need to be done before updating the
release documentation in stage 3.
* Merging up the changes in this release to newer branches, following the
* Merging up the changes in this release to newer branches, following the
SemVer pattern (e.g. 3.2.4 > 3.2 > 3.3 > 3 > master). The more often this is
done the easier it is, but this can sometimes be left for when you have
more free time. Branches not receiving regular stable versions anymore (e.g.
3.0 or 3.1) should usually be omitted.
3.0 or 3.1) can be omitted.
* Set the github milestones to completed, and create placeholders for the next
minor versions. It may be necessary to re-assign any issues assigned to the prior
milestones to these new ones.
* Make sure that the [releases page](https://github.com/silverstripe/silverstripe-installer/releases)
on github shows the new tag.
on github shows the new tag.
*Updating non-patch versions*
@ -327,7 +414,7 @@ Running either of these tasks may time out when requested, but will continue to
only the search index rebuild takes a long period of time.
Note that markdown is automatically updated daily, and this should only be done if an immediate refresh is necessary.
### Stage 3: Let the world know
Once a release has been published there are a few places where user documentation
@ -335,7 +422,7 @@ will need to be regularly updated.
* Make sure that the [download page](http://www.silverstripe.org/download) on
silverstripe.org has the release available. If it's a stable, it will appear
at the top of the page. If it's a pre-release, it will be available under the
at the top of the page. If it's a pre-release, it will be available under the
[development builds](http://www.silverstripe.org/download#download-releases)
section. If it's not available, you might need to check that the release was
properly uploaded to aws s3, or that you aren't viewing a cached version of
@ -344,13 +431,11 @@ will need to be regularly updated.
you can run the [CoreReleaseUpdateTask](http://www.silverstripe.org/dev/tasks/CoreReleaseUpdateTask)
to synchronise with packagist.
* Ensure that [docs.silverstripe.org](http://docs.silverstripe.org) has the
updated documentation by running the build task in the root folder. If
you do not have ssh access to this server, then contact a SilverStripe staff member
to update this for you. Make sure that the download link below links to the
correct changelog page. E.g.
[https://docs.silverstripe.org/en/3.2/changelogs/3.2.1/](https://docs.silverstripe.org/en/3.2/changelogs/3.2.1/)
* Post a release announcement on the [silverstripe release announcement](https://groups.google.com/forum/#!forum/silverstripe-announce)
google group.
updated documentation and the changelog link in your announcement works.
* Announce the release on the ["Releases" forum](https://forum.silverstripe.org/c/releases).
Needs to happen on every minor release for previous releases, see [supported versions](https://docs.silverstripe.org/en/4/contributing/release_process/#supported-versions)
* Announce any new EOLs for minor versions on the ["Releases" forum](https://forum.silverstripe.org/c/releases).
* Update the [roadmap](https://www.silverstripe.org/roadmap) with new dates for EOL versions ([CMS edit link](https://www.silverstripe.org/admin/pages/edit/EditForm/3103/field/TableComponentItems/item/670/edit))
* Update the [Slack](https://www.silverstripe.org/community/slack-signup/) topic to include the new release version.
* For major or minor releases: Work with SilverStripe marketing to get a blog post out.
They might choose to announce the release on social media as well.

View File

@ -134,7 +134,7 @@ This also applies for any modules staying compatible with SilverStripe 2.x.
## Contact
Translators have their own [mailinglist](https://groups.google.com/forum/#!forum/silverstripe-translators), but you can
Translators have their own [mailing list](https://groups.google.com/forum/#!forum/silverstripe-translators), but you can
also reach a core member on [IRC](https://irc.silverstripe.org). The transifex.com interface has a built-in discussion
board if you have specific comments on a translation.

View File

@ -7,8 +7,10 @@ The Core Committers team is reviewed approximately annually, new members are add
* [Chris Joe](https://github.com/flamerohr/)
* [Damian Mooyman](https://github.com/tractorcow/)
* [Daniel Hensby](https://github.com/dhensby)
* [Guy Marriott](https://github.com/ScopeyNZ)
* [Ingo Schommer](https://github.com/chillu)
* [Loz Calver](https://github.com/kinglozzer)
* [Maxime Rainville](https://github.com/maxime-rainville)
* [Paul Clarke](https://github.com/clarkepaul)
* [Robbie Averill](https://github.com/robbieaverill)
* [Sam Minnée](https://github.com/sminnee)

View File

@ -3,7 +3,7 @@ introduction: Any open source product is only as good as the community behind it
## House rules for everybody contributing to SilverStripe
* Read over the SilverStripe Community [Code of Conduct](code_of_conduct)
* Ask questions on the [forum](http://silverstripe.org/community/forums), and stick to more high-level discussions on the [core mailinglist](https://groups.google.com/forum/#!forum/silverstripe-dev)
* Ask questions on the [forum](http://silverstripe.org/community/forums)
* Make sure you know how to [raise good bug reports](issues_and_bugs)
* Everybody can contribute to SilverStripe! If you do, ensure you can [submit solid pull requests](code)

View File

@ -15,13 +15,13 @@ and play with the interactive [demo website](http://demo.silverstripe.org/).
SilverStripe has an wide range of options for getting support:
* Join our [forum](https://forum.silverstripe.org)
* Ask technical questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/silverstripe)
* Get help on our [Slack channel](https://www.silverstripe.org/community/slack-signup/)
* Read the technical reference in our [API Documentation](http://api.silverstripe.org/)
* Get a user-focused overview of the CMS features in our [User Help](http://userhelp.silverstripe.com)
* Discuss new features, API changes and the development [roadmap](http://www.silverstripe.org/software/roadmap/)
on [UserVoice](http://silverstripe.uservoice.com/forums/251266-new-features)
* Join our [core mailinglist](https://groups.google.com/forum/#!forum/silverstripe-dev)
## Building your first SilverStripe Web application

View File

@ -150,7 +150,4 @@ ar:
LOGIN: دخول
LOSTPASSWORDHEADER: 'كلمة مرور مفقودة'
NOTEPAGESECURED: 'هذه الصفحة محمية بكلمة مرور ، أدخل بيانات دخولك بالأسفل ليتم السماح لك بالوصول للصفحة'
NOTERESETLINKINVALID: "<p> رابط إعادة تعيين كلمة المرور غير صحيح أو نفذت صلاحيته.</p>\n<p> \nيمكنك طلب رابط جديد <\"{a href=\"{link1\"> هنا </a>\n أو تغيير كلمة المرور الخاصة بك بعد <\"{a href=\"{link2\"> تسجيل دخولك</a>.\n</p>"
NOTERESETPASSWORD: 'أدخل بريدك الإلكتروني و سيتم إرسال رابط إعادة تهيئة كلمة المرور '
PASSWORDSENTHEADER: 'رابط استعادة كلمة المرور تم إرساله إلى ''{بريدك}'''
PASSWORDSENTTEXT: 'شكرا لك! تم إرسال رابط إعادة تعيين إلى ''{بريدك}''، بشرط وجود حساب قائم بالنسبة لعنوان هذا البريد الإلكتروني .'

View File

@ -313,7 +313,4 @@ bg:
LOGOUT: Изход
LOSTPASSWORDHEADER: 'Забравена парола'
NOTEPAGESECURED: 'Тази страница е защитена. Въведете вашите данни по-долу, за да продължите.'
NOTERESETLINKINVALID: '<p>Връзката за нулиране на парола не е вярна или е просрочена.</p><p>Можете да заявите нова <a href="{link1}">тук</a> или да промените паролата си след като <a href="{link2}">влезете</a>.</p>'
NOTERESETPASSWORD: 'Въведете вашият email адрес и ще ви изпратим линк, с който ще можете да смените паролата си'
PASSWORDSENTHEADER: 'Връзка за нулиране на парола беше изпратена на ''{email}'''
PASSWORDSENTTEXT: 'Благодарим ви! Връзка за нулиране на паролата беше изпратен на ''{email}'', ако съществува акаунт с този имейл адрес.'

View File

@ -194,7 +194,4 @@ cs:
LOGIN: Přihlásit
LOSTPASSWORDHEADER: 'Zapomenuté heslo'
NOTEPAGESECURED: 'Tato stránka je zabezpečená. Vložte své přihlašovací údaje a my Vám zároveň pošleme práva.'
NOTERESETLINKINVALID: '<p>Odkaz na resetování hesla není platný nebo je prošlý.</p><p>Můžete požádat o nový <a href="{link1}">zde</a> nebo změňte své heslo až <a href="{link2}">se přihlásíte</a>.</p>'
NOTERESETPASSWORD: 'Zadejte svou e-mailovou adresu a bude vám zaslán nulovací odkaz pro Vaše heslo'
PASSWORDSENTHEADER: 'Odkaz na resetování hesla byl odeslán na ''{email}'''
PASSWORDSENTTEXT: 'Děkujeme! Resetovací odkaz byl odeslán na ''{email}'', pokud účet existuje pro tuto emailovou adresu.'

View File

@ -1,5 +1,337 @@
da:
SilverStripe\Admin\LeftAndMain:
VersionUnknown: ukendt
SilverStripe\AssetAdmin\Forms\UploadField:
Dimensions: Dimensioner
EDIT: Rediger
EDITINFO: 'Rediger denne fil'
REMOVE: Fjern
SilverStripe\Control\ChangePasswordEmail_ss:
CHANGEPASSWORDFOREMAIL: 'Koden for kontoen med email addressen {email} er ændret. Hvis du ikke har skiftet din kode, så skift venligst din kode ved at klikke på linket herunder'
CHANGEPASSWORDTEXT1: 'Du skiftede dit kodeord for'
CHANGEPASSWORDTEXT3: 'Skift kodeord'
HELLO: Hej
SilverStripe\Control\Email\ForgotPasswordEmail_ss:
HELLO: Hej
TEXT1: 'Her er din'
TEXT2: 'link til at nulstille dit kodeord'
TEXT3: for
SilverStripe\Control\RequestProcessor:
INVALID_REQUEST: 'Ugyldig forespørgsel'
REQUEST_ABORTED: 'Forespørgsel annulleret'
SilverStripe\Core\Manifest\VersionProvider:
VERSIONUNKNOWN: Ukendt
SilverStripe\Forms\CheckboxField:
NOANSWER: Nej
YESANSWER: Ja
SilverStripe\Forms\CheckboxSetField_ss:
NOOPTIONSAVAILABLE: 'Ingen tilgængelige muligheder'
SilverStripe\Forms\ConfirmedPasswordField:
ATLEAST: 'Kodeord skal være mindst {min} tegn lang.'
BETWEEN: 'Kodeord skal være {min} til {max} karakterer lang.'
CURRENT_PASSWORD_ERROR: 'Det nuværende kodeord du har indtastet er ikke korrekt.'
CURRENT_PASSWORD_MISSING: 'Du skal indtaste dit nuværende kodeord.'
LOGGED_IN_ERROR: 'Du skal være logget ind for at skifte dit kodeord.'
MAXIMUM: 'Kodeord må maks være {max} tegn lang'
SHOWONCLICKTITLE: 'Skift kodeord'
SilverStripe\Forms\CurrencyField:
CURRENCYSYMBOL: DKK
SilverStripe\Forms\DateField:
VALIDDATEFORMAT2: 'Indtats venligst et gyldigt datoformat ({format})'
VALIDDATEMAXDATE: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({date})'
VALIDDATEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato ({date})'
SilverStripe\Forms\DatetimeField:
VALIDDATEMAXDATETIME: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({datetime})'
VALIDDATETIMEFORMAT: 'Indtats venligst et gyldigt dato- og tidsformat ({format})'
VALIDDATETIMEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato og tid ({datetime})'
SilverStripe\Forms\DropdownField:
CHOOSE: (Vælg)
CHOOSE_MODEL: '(Vælg {name})'
SOURCE_VALIDATION: 'Venligst vælg en eksisterende værdi fra listen. {value} er ikke en tilladt mulighed'
SilverStripe\Forms\EmailField:
VALIDATION: 'Indtast venligst en emailadresse'
SilverStripe\Forms\FileUploadReceiver:
FIELDNOTSET: 'Fil information ikke fundet'
SilverStripe\Forms\Form:
BAD_METHOD: 'Denne form kræver en {method} indsendelse'
CSRF_EXPIRED_MESSAGE: 'Din session er udløbet. Venligst gensend formularen.'
CSRF_FAILED_MESSAGE: 'Det ser ud til der har været et teknisk problem. Klik venligst på tilbageknappen, tryk opdater i din browser og prøv igen.'
VALIDATIONPASSWORDSDONTMATCH: 'Kodeordene er ikke identiske'
VALIDATIONPASSWORDSNOTEMPTY: 'Kodeord kan ikke være tomme'
VALIDATIONSTRONGPASSWORD: 'Kodeord skal mindst have et tal og et alfanumerisk tegn'
VALIDATOR: Validering
VALIDCURRENCY: 'Indtast venligst en gyldig valuta'
SilverStripe\Forms\FormField:
EXAMPLE: 'f.eks. {format}'
NONE: ingen
SilverStripe\Forms\FormScaffolder:
TABMAIN: Primær
SilverStripe\Forms\GridField\GridField:
Filter: Filter
Add: 'Tilføj {name}'
CSVEXPORT: 'Eksporter til CSV'
CSVIMPORT: 'Importer CSV'
Filter: Filtrer
FilterBy: 'Filtrer på'
Find: Find
LinkExisting: 'Link eksisterende'
NewRecord: 'Ny {type}'
NoItemsFound: 'Ingen elementer fundet'
PRINTEDAT: 'Printet d.'
PRINTEDBY: 'Printet af'
PlaceHolder: 'Find {type}'
PlaceHolderWithLabels: 'Find {type} på {name}'
Print: Print
RelationSearch: Relationssøgning
ResetFilter: Nulstil
SilverStripe\Forms\GridField\GridFieldDeleteAction:
DELETE_DESCRIPTION: Slet
Delete: Slet
DeletePermissionsFailure: 'Ingen slette rettigheder'
EditPermissionsFailure: 'Ingen rettighed til at fjerne emnet'
UnlinkRelation: Fjern
SilverStripe\Forms\GridField\GridFieldDetailForm:
CancelBtn: Annuller
Create: Opret
Delete: Slet
DeletePermissionsFailure: 'Ingen slette rettigheder'
Deleted: 'Slet {type} {name}'
Save: Gem
SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: Rediger
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Kan ikke fjerne dig selv fra denne gruppe, du vil miste administrator rettigheder'
SilverStripe\Forms\GridField\GridFieldPaginator:
OF: af
Page: Side
View: Vis
SilverStripe\Forms\GridField\GridFieldVersionedState:
ADDEDTODRAFTHELP: 'Elementet er endnu ikke udgivet'
ADDEDTODRAFTSHORT: Kladde
ARCHIVEDPAGEHELP: 'Elementet er fjernet fra kladde og udgivet version'
ARCHIVEDPAGESHORT: Arkiveret
MODIFIEDONDRAFTHELP: 'Elementet har ikke udgivne ændringer'
MODIFIEDONDRAFTSHORT: Ændret
ONLIVEONLYSHORT: 'Kun på udgivet version'
ONLIVEONLYSHORTHELP: 'Elementet er udgivet, men er slette fra kladde'
SilverStripe\Forms\MoneyField:
FIELDLABELAMOUNT: Beløb
FIELDLABELCURRENCY: Valuta
INVALID_CURRENCY: 'Valuta {currency} er ikke i listen over tilladte valutaer'
SilverStripe\Forms\MultiSelectField:
SOURCE_VALIDATION: 'Vælg venligst eksisterende værdier fra listen. Ugyldig mulighed(er) {value} valgt'
SilverStripe\Forms\NullableField:
IsNullLabel: 'Er Null'
SilverStripe\Forms\NumericField:
VALIDATION: '''{value}'' er ikke et tal, kun tal accepteres i dette felt'
SilverStripe\Forms\TimeField:
VALIDATEFORMAT: 'Indtats venligst et gyldigt tidsformat ({format})'
SilverStripe\ORM\DataObject:
PLURALNAME: Dataobjekter
PLURALS:
one: 'Et dataobjekt'
other: '{count} dataobjekter'
SINGULARNAME: Dataobjekt
SilverStripe\ORM\FieldType\DBBoolean:
ANY: Enhver
NOANSWER: Nej
YESANSWER: Ja
SilverStripe\ORM\FieldType\DBDate:
DAYS_SHORT_PLURALS:
one: '{count} dag'
other: '{count} dage'
HOURS_SHORT_PLURALS:
one: '{count} time'
other: '{count} timer'
LessThanMinuteAgo: 'mindre end et minut'
MINUTES_SHORT_PLURALS:
one: '{count} minut'
other: '{count} minutter'
MONTHS_SHORT_PLURALS:
one: '{count} måned'
other: '{count} måneder'
SECONDS_SHORT_PLURALS:
one: '{count} sekund'
other: '{count} sekunder'
TIMEDIFFAGO: '{difference} siden'
TIMEDIFFIN: 'i {difference}'
YEARS_SHORT_PLURALS:
one: '{count} år'
other: '{count} år'
SilverStripe\ORM\FieldType\DBEnum:
ANY: Enhver
SilverStripe\ORM\Hierarchy:
LIMITED_TITLE: 'For mange underelementer ({count})'
SilverStripe\ORM\Hierarchy\Hierarchy:
InfiniteLoopNotAllowed: 'Uendeligt løkke fundet i "{type}" hierarkiet. Ændre venligst det overliggende element for at løse dette'
LIMITED_TITLE: 'For mange underelementer ({count})'
SilverStripe\ORM\ValidationException:
DEFAULT_ERROR: Valideringsfejl
SilverStripe\Security\BasicAuth:
ENTERINFO: 'Indtast venligst et brugernavn og kodeord.'
ERRORNOTADMIN: 'Den bruger er ikke en administrator.'
ERRORNOTREC: 'Brugernavn / kodeord kunne ikke genkendes'
SilverStripe\Security\CMSMemberLoginForm:
PASSWORDEXPIRED: '<p>Dit kodeord er udløbet. <a target="_top" href="{link}">Vælg venligst et nyt.</a></p>'
SilverStripe\Security\CMSSecurity:
INVALIDUSER: '<p>Ugyldig bruger. <a target="_top" href="{link}">Log venligst ind igen her</a> for at fortsætte.</p>'
LOGIN_MESSAGE: '<p>Din session er løbet ud pga. inaktivitet</p>'
LOGIN_TITLE: 'Log ind igen, for at fortsætte hvor du slap.'
SUCCESS: Succes
SUCCESSCONTENT: '<p>Logget ind. Hvis du ikke automatisk viderestilles så <a target="_top" href="{link}">klik her</a></p>'
SUCCESS_TITLE: 'Logget ind med sucess'
SilverStripe\Security\DefaultAdminService:
DefaultAdminFirstname: 'Standard admin'
SilverStripe\Security\Group:
AddRole: 'Tilføj en rolle for denne gruppe'
Code: 'Gruppe kode'
DefaultGroupTitleAdministrators: Administratorer
DefaultGroupTitleContentAuthors: Indholdsforfattere
Description: Beskrivelse
GROUPNAME: Gruppenavn
GroupReminder: 'Hvis du vælger en overliggende gruppe, får denne gruppe alle dens roller'
HierarchyPermsError: 'Kan ikke tildele overliggende gruppe "{group}" med fortrinsrettigheder (kræver ADMIN adgang)'
Locked: 'Låst?'
MEMBERS: Brugere
NEWGROUP: 'Ny gruppe'
NoRoles: 'Ingen roller fundet'
PERMISSIONS: Rettigheder
PLURALNAME: Grupper
PLURALS:
one: 'En gruppe'
other: '{count} grupper'
Parent: 'Overliggende gruppe'
ROLES: Roller
ROLESDESCRIPTION: 'Roller er et prædefineret sæt af rettigheder, som kan tildeles grupper.<br />De bliver nedarvet fra en overliggende grupper hvis krævet.'
RolesAddEditLink: 'Administrer roller'
SINGULARNAME: Gruppe
Sort: Sortering
has_many_Permissions: Rettigheder
many_many_Members: Brugere
SilverStripe\Security\LoginAttempt:
Email: 'Email adresse'
EmailHashed: 'Email adresse (hashed)'
IP: 'IP addresse'
PLURALNAME: Loginforsøg
PLURALS:
one: 'Et loginforsøg'
other: '{count} loginforsøg'
SINGULARNAME: 'Login forsøg'
Status: Status
SilverStripe\Security\Member:
ADDGROUP: 'Tilføj gruppe'
BUTTONCHANGEPASSWORD: 'Skift kodeord'
BUTTONLOGIN: 'Log ind'
BUTTONLOGINOTHER: 'Log ind med en anden bruger'
BUTTONLOGOUT: 'Log ud'
BUTTONLOSTPASSWORD: 'Jeg har glemt mit kodeord'
CONFIRMNEWPASSWORD: 'Bekræft nyt kodeord'
CONFIRMPASSWORD: 'Bekræft kodeord'
CURRENT_PASSWORD: 'Nuværende kodeord'
EDIT_PASSWORD: 'Nyt kodeord'
EMAIL: Email
EMPTYNEWPASSWORD: 'Det nye kodeord kan ikke være tom, prøv venligst igen'
ENTEREMAIL: 'Indtast venligst en email adresse for at få et nulstillingslink.'
ERRORLOCKEDOUT2: 'Din konto er blevet midlertidigt deaktiveret pga. for mange fejlslagne loginforsøg. Forsøg venligst igen om {count} minutter.'
ERRORNEWPASSWORD: 'Du har indtastet dit nye kodeord forskelligt, forsøg igen'
ERRORPASSWORDNOTMATCH: 'Dit nuværende kodeord matcher ikke, forsøg venligst igen'
ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.'
FIRSTNAME: Fornavn
INTERFACELANG: 'Sprog i brugerfladen'
KEEPMESIGNEDIN: 'Hold mig logget ind'
LOGGEDINAS: 'Du er logget ind som {name}.'
NEWPASSWORD: 'Nyt kodeord'
PASSWORD: Kodeord
PASSWORDEXPIRED: 'Dit kodeord er udløbet. Vælg venligst et nyt.'
PLURALNAME: Brugere
PLURALS:
one: 'En bruger'
other: '{count} brugere'
REMEMBERME: 'Husk mig til næste gang? (i {count} dage på denne enhed)'
SINGULARNAME: Bruger
SUBJECTPASSWORDCHANGED: 'Dit kodeord er blevet ændret'
SUBJECTPASSWORDRESET: 'Link til at nulstille dit kodeord'
SURNAME: Efternavn
VALIDATIONADMINLOSTACCESS: 'Kan ikke fjerne alle admin grupper fra din profil'
ValidationIdentifierFailed: 'Kan ikke overskrive eksisterende bruger #{id} med identisk identifikator ({name} = {value}))'
WELCOMEBACK: 'Velkommen tilbage, {firstname}'
YOUROLDPASSWORD: 'Dit gamle kodeord'
belongs_many_many_Groups: Grupper
db_Locale: 'Sprog i brugerfladen'
db_LockedOutUntil: 'Låst ude indtil'
db_Password: Kodeord
db_PasswordExpiry: Kodeordsudløbsdato
SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm:
AUTHENTICATORNAME: 'CMS bruger loginform'
BUTTONFORGOTPASSWORD: 'Glemt kodeord'
BUTTONLOGIN: 'Log mig ind igen'
BUTTONLOGOUT: 'Log ud'
SilverStripe\Security\MemberAuthenticator\MemberAuthenticator:
ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.'
NoPassword: 'Der er ikke en kode på denne bruger.'
SilverStripe\Security\MemberAuthenticator\MemberLoginForm:
AUTHENTICATORNAME: 'Email og kodeord'
SilverStripe\Security\MemberPassword:
PLURALNAME: 'Bruger kodeord'
PLURALS:
one: 'Et bruger kodeord'
other: '{count} bruger kodeord'
SINGULARNAME: 'Bruger kodeord'
SilverStripe\Security\PasswordValidator:
LOWCHARSTRENGTH: 'Forøg venligst kodeordets styrke, ved at tilføje nogle af følgende tegn: {chars}'
PREVPASSWORD: 'Du har tidligere brugt dette kodeord, vælg venligst et nyt kodeord'
TOOSHORT: 'Kodeordet er for kort, det skal mindst være {minimum} eller flere tegn langt'
SilverStripe\Security\Permission:
AdminGroup: Administrator
CMS_ACCESS_CATEGORY: 'CMS Adgang'
CONTENT_CATEGORY: Indholdsrettigheder
FULLADMINRIGHTS: 'Fuld administrator rettighed'
FULLADMINRIGHTS_HELP: 'Indebærer og overskriver alle andre tildelte rettigheder.'
PERMISSIONS_CATEGORY: 'Roller og adgangsrettigheder'
PLURALNAME: Rettigheder
PLURALS:
one: 'En rettighed'
other: '{count} rettigheder'
SINGULARNAME: Rettighed
UserPermissionsIntro: 'Tildeling af grupper til denne bruger, ændrer de rettigheder brugeren har. Se gruppe området for rettigheds detaljer på de individuelle grupper.'
SilverStripe\Security\PermissionCheckboxSetField:
AssignedTo: 'tildelt til "{title}"'
FromGroup: 'nedarvet fra gruppen "{title}"'
FromRole: 'nedarvet fra rollen "{title}"'
FromRoleOnGroup: 'nedarvet fra rollen "{roletitle}" på gruppen "{grouptitle}"'
SilverStripe\Security\PermissionRole:
OnlyAdminCanApply: 'Kun administratorer kan tilføje'
PLURALNAME: Roller
PLURALS:
one: 'En rolle'
other: '{count} roller'
SINGULARNAME: Rolle
Title: Titel
SilverStripe\Security\PermissionRoleCode:
PLURALNAME: 'Rettigheds rolle koder'
PLURALS:
one: 'En rettigheds rolle kode'
other: '{count} rettigheds rolle koder'
PermsError: 'Kan ikke tildele koden "{code}" med fortrinsrettigheder (kræver ADMIN adgang)'
SINGULARNAME: 'Rettighed rolle kode'
SilverStripe\Security\RememberLoginHash:
PLURALNAME: 'Login hashes'
PLURALS:
one: 'Et login hash'
other: '{count} Login Hashes'
SINGULARNAME: 'Login hash'
SilverStripe\Security\Security:
ALREADYLOGGEDIN: 'Du har ikke adgang til denne side. Hvis du har en anden bruger der har adgang til denne side, kan du logge ind med denne herunder.'
BUTTONSEND: 'Send mig linket til at nulstille kodeordet'
CHANGEPASSWORDBELOW: 'Du kan ændre dit kodeord herunder.'
CHANGEPASSWORDHEADER: 'Skift dit kodeord'
CONFIRMLOGOUT: 'Klik venligst på knappen herunder, for at bekræfte at du vil logge ud.'
ENTERNEWPASSWORD: 'Indtast venligst et nyt kodeord.'
ERRORPASSWORDPERMISSION: 'Du skal være logget ind, for at kunne ændre dit kodeord!'
LOGIN: 'Log ind'
LOGOUT: 'Log ud'
LOSTPASSWORDHEADER: 'Glemt kodeord'
NOTEPAGESECURED: 'Denne side er beskyttet. Indtast dine loginoplysninger herunder for at få adgang.'
NOTERESETLINKINVALID: '<p>Kodeordets nulstillingslink er ugyldigt eller udløbet.</p><p>Du kan anmode om et nyt link <a href="{link1}">her</a> eller skifte dit kodeord efter du er <a href="{link2}">logget ind</a>.</p>'
NOTERESETPASSWORD: 'Indtast din email adresse, så sender vi dig et link som du kan nulstille dit kodeord med'
PASSWORDSENTHEADER: 'Link til nulstilling af kodeord er sendt til ''{email}'''
PASSWORDSENTTEXT: 'Tak for det! Et link til at nulstille kodeordet er sendt til ''{email}'', forudsat at en konto eksisterer med denne email adresse.'

View File

@ -190,7 +190,4 @@ de:
LOGIN: Anmelden
LOSTPASSWORDHEADER: 'Passwort vergessen'
NOTEPAGESECURED: 'Diese Seite ist geschützt. Bitte melden Sie sich an und Sie werden sofort weitergeleitet.'
NOTERESETLINKINVALID: '<p>Der Link zum Zurücksetzen des Passworts ist entweder nicht korrekt oder abgelaufen</p><p>Sie können <a href="{link1}">einen neuen Link anfordern</a> oder Ihr Passwort nach dem <a href="{link2}">einloggen</a> ändern.</p>'
NOTERESETPASSWORD: 'Geben Sie Ihre E-Mail-Adresse ein und wir werden Ihnen einen Link zuschicken, mit dem Sie Ihr Passwort zurücksetzen können.'
PASSWORDSENTHEADER: 'Der Link zum Zurücksetzen des Passworts wurde an ''{email}'' gesendet'
PASSWORDSENTTEXT: 'Vielen Dank! Wenn ein Account zu der E-Mail Adresse ''{email}'' existiert, wurde eine E-Mail mit dem Link zum Zurücksetzen des Passworts verschickt.'

153
lang/de_DE.yml Normal file
View File

@ -0,0 +1,153 @@
de_DE:
SilverStripe\Admin\LeftAndMain:
VersionUnknown: Unbekannt
SilverStripe\AssetAdmin\Forms\UploadField:
Dimensions: Maße
EDIT: Bearbeiten
EDITINFO: 'Datei bearbeiten'
REMOVE: Entfernen
SilverStripe\Control\ChangePasswordEmail_ss:
CHANGEPASSWORDFOREMAIL: 'Das Passwort für das Konto mit der E-Mail-Adresse {email} wurde geändert. Wenn Sie Ihr Passwort nicht geändert haben, ändern Sie bitte Ihr Passwort mit dem folgenden Link'
CHANGEPASSWORDTEXT1: 'Sie haben Ihr Passwort für geändert'
CHANGEPASSWORDTEXT3: 'Passwort ändern'
HELLO: Hallo
SilverStripe\Control\Email\ForgotPasswordEmail_ss:
HELLO: Hallo
TEXT1: 'Hier ist dein'
TEXT2: 'Link zum Zurücksetzen des Passworts'
TEXT3: für
SilverStripe\Core\Manifest\VersionProvider:
VERSIONUNKNOWN: Unbekannt
SilverStripe\Forms\CheckboxField:
NOANSWER: Nein
YESANSWER: Ja
SilverStripe\Forms\CheckboxSetField_ss:
NOOPTIONSAVAILABLE: 'Keine Optionen vorhanden'
SilverStripe\Forms\ConfirmedPasswordField:
ATLEAST: 'Passwörter müssen mindestens {min} Zeichen lang sein.'
BETWEEN: 'Passwörter müssen {min} bis {max} Zeichen lang sein.'
SHOWONCLICKTITLE: 'Passwort ändern'
SilverStripe\Forms\DropdownField:
CHOOSE: (Auswählen)
CHOOSE_MODEL: '({name} auswählen)'
SilverStripe\Forms\GridField\GridField:
Add: '{name} hinzufügen'
CSVEXPORT: 'Als CSV exportieren'
CSVIMPORT: 'CSV importieren'
Filter: Filtern
FilterBy: 'Filtern nach'
Print: Drucken
ResetFilter: Zurücksetzen
SilverStripe\Forms\GridField\GridFieldDeleteAction:
Delete: Löschen
SilverStripe\Forms\GridField\GridFieldDetailForm:
CancelBtn: Abbrechen
Save: Speichern
SilverStripe\Forms\GridField\GridFieldPaginator:
Page: Seite
SilverStripe\Forms\MoneyField:
FIELDLABELAMOUNT: Betrag
FIELDLABELCURRENCY: Währung
INVALID_CURRENCY: 'Die Währung {currency} ist nicht in der Liste der erlauben Währungen'
SilverStripe\ORM\DataObject:
PLURALNAME: DatenObjekte
PLURALS:
one: 'Ein DatenObjekt'
other: '{count} DatenObjekte'
SINGULARNAME: DatenObjekt
SilverStripe\ORM\FieldType\DBBoolean:
NOANSWER: Nein
YESANSWER: Ja
SilverStripe\ORM\FieldType\DBDate:
DAYS_SHORT_PLURALS:
one: '{count} Tag'
other: '{count} Tage'
HOURS_SHORT_PLURALS:
one: '{count} Stunde'
other: '{count} Stunden'
LessThanMinuteAgo: 'weniger als 1 Minute'
MINUTES_SHORT_PLURALS:
one: '{count} Minute'
other: '{count} Minuten'
MONTHS_SHORT_PLURALS:
one: '{count} Monat'
other: '{count} Monate'
SECONDS_SHORT_PLURALS:
one: '{count} Sekunde'
other: '{count} Sekunden'
TIMEDIFFAGO: 'vor {difference}'
TIMEDIFFIN: 'in {difference}'
YEARS_SHORT_PLURALS:
one: '{count} Jahr'
other: '{count} Jahre'
SilverStripe\Security\BasicAuth:
ENTERINFO: 'Bitte geben Sie einen Benutzernamen und ein Passwort ein'
ERRORNOTADMIN: 'Dieser Benutzer ist kein Administrator'
ERRORNOTREC: 'Dieser Benutzer bzw. dieses Passwort ist unbekannt'
SilverStripe\Security\CMSMemberLoginForm:
PASSWORDEXPIRED: '<p>Ihr Passwort ist abgelaufen. <a target="_top" href="{link}">Bitte wählen Sie ein neues Passwort.</a></p>'
SilverStripe\Security\CMSSecurity:
SUCCESS_TITLE: 'Login erfolgreich'
SilverStripe\Security\DefaultAdminService:
DefaultAdminFirstname: 'Standard Administrator'
SilverStripe\Security\Group:
DefaultGroupTitleAdministrators: Administratoren
DefaultGroupTitleContentAuthors: Inhaltsautoren
Description: Beschreibung
GROUPNAME: Gruppenname
MEMBERS: Mitglieder
NEWGROUP: 'Neue Gruppe'
NoRoles: 'Keine Rollen gefunden'
PLURALNAME: Gruppen
PLURALS:
one: 'Eine Gruppe'
other: '{count} Gruppen'
SINGULARNAME: Gruppe
Sort: Sortierreihenfolge
has_many_Permissions: Rechte
many_many_Members: Mitglieder
SilverStripe\Security\LoginAttempt:
Email: 'E-Mail Adresse'
EmailHashed: 'E-Mail Adresse (gehasht)'
IP: 'IP Adresse'
PLURALNAME: Loginversuche
PLURALS:
one: 'Ein Loginversuch'
other: '{count} Loginversuche'
SINGULARNAME: Loginversuch
Status: Status
SilverStripe\Security\Member:
ADDGROUP: 'Gruppe hinzufügen'
BUTTONCHANGEPASSWORD: 'Passwort ändern'
BUTTONLOGIN: Einloggen
BUTTONLOGINOTHER: 'Als jemand anderes einloggen'
BUTTONLOGOUT: Ausloggen
BUTTONLOSTPASSWORD: 'Ich habe mein Passwort vergessen'
CONFIRMNEWPASSWORD: 'Neues Passwort bestätigen'
CONFIRMPASSWORD: 'Passwort bestätigen'
CURRENT_PASSWORD: 'Derzeitiges Passwort'
EDIT_PASSWORD: 'Neues Passwort'
EMAIL: E-Mail
EMPTYNEWPASSWORD: 'Das neue Passwort kann nicht leer sein, bitte versuchen Sie es erneut'
ENTEREMAIL: 'Bitte geben Sie Ihre E-Mail Adresse an. Sie erhalten eine E-Mail mit einem Link zum Zurücksetzen des Passworts.'
FIRSTNAME: Vorname
NEWPASSWORD: 'Neues Passwort'
PASSWORD: Passwort
PASSWORDEXPIRED: 'Ihr Passwort ist abgelaufen. Bitte wählen Sie ein neues Passwort.'
PLURALNAME: Mitglieder
PLURALS:
one: 'Ein Mitglied'
other: '{count} Mitglieder'
SINGULARNAME: Mitglied
SURNAME: Nachname
WELCOMEBACK: 'Willkommen zurück, {firstname}'
YOUROLDPASSWORD: 'Ihr altes Passwort'
belongs_many_many_Groups: Grouppen
db_Password: Passwort
SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm:
BUTTONFORGOTPASSWORD: 'Passwort vergessen'
SilverStripe\Security\Permission:
AdminGroup: Administrator
SilverStripe\Security\Security:
LOGIN: Einloggen
LOGOUT: Ausloggen

View File

@ -78,6 +78,7 @@ en:
LinkExisting: 'Link Existing'
NewRecord: 'New {type}'
NoItemsFound: 'No items found'
OpenFilter: 'Open search and filter'
PRINTEDAT: 'Printed at'
PRINTEDBY: 'Printed by'
PlaceHolder: 'Find {type}'
@ -85,10 +86,6 @@ en:
Print: Print
RelationSearch: 'Relation search'
ResetFilter: Reset
OpenFilter: 'Open search and filter'
SilverStripe\Forms\GridField\GridFieldFilterHeader:
Search: 'Search "{name}"'
SearchFormFaliure: 'No search form could be generated'
SilverStripe\Forms\GridField\GridFieldDeleteAction:
Delete: Delete
DeletePermissionsFailure: 'No delete permissions'
@ -103,6 +100,9 @@ en:
Save: Save
SilverStripe\Forms\GridField\GridFieldEditButton:
EDIT: Edit
SilverStripe\Forms\GridField\GridFieldFilterHeader:
Search: 'Search "{name}"'
SearchFormFaliure: 'No search form could be generated'
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Cannot remove yourself from this group, you will lose admin rights'
SilverStripe\Forms\GridField\GridFieldPaginator:

View File

@ -84,6 +84,7 @@ eo:
RelationSearch: 'Serĉi rilatojn'
ResetFilter: Restartigi
SilverStripe\Forms\GridField\GridFieldDeleteAction:
DELETE_DESCRIPTION: Forigi
Delete: Forigi
DeletePermissionsFailure: 'Mankas permeso forigi'
EditPermissionsFailure: 'Mankas permeso malligi rikordon'
@ -95,12 +96,23 @@ eo:
DeletePermissionsFailure: 'Mankas permeso forigi'
Deleted: 'Forigita {type} {name}'
Save: Konservi
SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: Redakti
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Ne povas forigi vin el ĉi tiu grupo; vi perdus administrajn rajtojn'
SilverStripe\Forms\GridField\GridFieldPaginator:
OF: de
Page: Paĝo
View: Vido
SilverStripe\Forms\GridField\GridFieldVersionedState:
ADDEDTODRAFTHELP: 'Ero ankoraŭ estas ne publikigita'
ADDEDTODRAFTSHORT: Malneto
ARCHIVEDPAGEHELP: 'Ero estas forigita el malneta kaj publika'
ARCHIVEDPAGESHORT: Enarkivigita
MODIFIEDONDRAFTHELP: 'Ero enhavas nepublikigitajn ŝanĝojn'
MODIFIEDONDRAFTSHORT: Ŝanĝita
ONLIVEONLYSHORT: 'Nur ĉe publika'
ONLIVEONLYSHORTHELP: 'Ero estas publikigita, sed ĝi estas forigita el malneto'
SilverStripe\Forms\MoneyField:
FIELDLABELAMOUNT: Kvanto
FIELDLABELCURRENCY: Kurzo

View File

@ -249,7 +249,4 @@ es:
LOGIN: Entrar
LOSTPASSWORDHEADER: '¿Contraseña Perdida?'
NOTEPAGESECURED: 'Esa página está protegida. Introduzca sus datos de acreditación a continuación y lo enviaremos a ella en un momento.'
NOTERESETLINKINVALID: '<p>El enlace para restablecer la contraseña es inválido o ha expirado.</p><p>Usted puede solicitar uno nuevo <a href="{link1}">aqui</a> o cambiar su contraseña después de que se haya <a href="{link2}">conectado</a>.</p>'
NOTERESETPASSWORD: 'Introduzca su dirección de e-mail, y le enviaremos un enlace, con el cual podrá restaurar su contraseña'
PASSWORDSENTHEADER: 'Un enlace para restablecer la contraseña ha sido enviado a ''{email}'''
PASSWORDSENTTEXT: 'Gracias! Un enlace para restablecer la contraseña ha sido enviado a ''{email}'', siempre que una cuenta exista para la dirección de email indicada.'

View File

@ -139,7 +139,4 @@ et_EE:
ERRORPASSWORDPERMISSION: 'Pead olema sisseloginud, et parooli muuta!'
LOGIN: 'Logi sisse'
NOTEPAGESECURED: 'See leht on turvatud. Sisesta enda andmed allpool ja me saadame sind otse edasi'
NOTERESETLINKINVALID: '<p>Parooli lähtestamise link on kehtetu või aegunud.</p><p>Saate taotleda uut linki <a href="{link1}">siin</a> või muuta parooli pärast <a href="{link2}">sisselogimist</a>.</p>'
NOTERESETPASSWORD: 'Sisesta oma email ja me saadame sulle lingi kus saad oma parooli tühistada.'
PASSWORDSENTHEADER: 'Parooli lähtestamise link saadeti aadressile ''{email}'''
PASSWORDSENTTEXT: 'Aitäh! Lähtestamislink saadeti aadressile ''{email}'' eeldusel, et selle e-posti aadressiga seotud konto on olemas.'

View File

@ -168,4 +168,3 @@ fa_IR:
ERRORPASSWORDPERMISSION: 'جهت تغییر گذرواژه خود باید وارد شده باشید!'
LOGIN: ورود
LOSTPASSWORDHEADER: 'فراموشی گذرواژه'
PASSWORDSENTHEADER: 'لینک ازنوسازی گذرواژه به ''{email}'' ارسال شد'

View File

@ -197,6 +197,7 @@ fi:
many_many_Members: Jäsenet
SilverStripe\Security\LoginAttempt:
Email: Sähköpostiosoite
EmailHashed: 'Sähköpostiosoite (tiivistetty)'
IP: IP-osoite
PLURALNAME: Kirjautumisyritykset
PLURALS:
@ -255,6 +256,8 @@ fi:
SilverStripe\Security\MemberAuthenticator\MemberAuthenticator:
ERRORWRONGCRED: 'Antamasi tiedot eivät näytä oikeilta. Yritä uudelleen.'
NoPassword: 'Tällä käyttäjällä ei ole salasanaa'
SilverStripe\Security\MemberAuthenticator\MemberLoginForm:
AUTHENTICATORNAME: 'Sähköpostiosoite & salasana'
SilverStripe\Security\MemberPassword:
PLURALNAME: 'Käyttäjän salasanat'
PLURALS:
@ -316,7 +319,4 @@ fi:
LOGOUT: 'Kirjaudu ulos'
LOSTPASSWORDHEADER: 'Unohtunut salasana'
NOTEPAGESECURED: 'Tämä sivu on suojattu. Syötä tunnistetietosi alle niin pääset eteenpäin.'
NOTERESETLINKINVALID: '<p>Salasanan palautuslinkki on virheellinen tai vanhentunut.</p><p>Voit pyytää uuden <a href="{link1}">napsauttamalla tästä</a> tai vaihtaa salasanasi <a href="{link2}">kirjautumisen jälkeen</a>.</p>'
NOTERESETPASSWORD: 'Syötä sähköpostiosoitteesi ja lähetämme sinulle linkin, jonka avulla saat palautettua salasanasi'
PASSWORDSENTHEADER: 'Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}'''
PASSWORDSENTTEXT: 'Kiitos! Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}'', joka on liitettynä tähän käyttäjätiliin.'

View File

@ -84,7 +84,6 @@ fr:
RelationSearch: 'Rechercher relations'
ResetFilter: Réinitialiser
SilverStripe\Forms\GridField\GridFieldDeleteAction:
DELETE_DESCRIPTION: Supprimer
Delete: Supprimer
DeletePermissionsFailure: 'Vous navez pas les autorisations pour supprimer'
EditPermissionsFailure: 'Pas de permissions pour délier l''enregistrement'
@ -96,8 +95,6 @@ fr:
DeletePermissionsFailure: 'Vous navez pas les autorisations pour supprimer'
Deleted: '{type} {name} supprimés'
Save: Enregistrer
SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: Éditer
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Impossible de retirer votre propre profil de ce groupe, vous perdriez vos droits d''administration'
SilverStripe\Forms\GridField\GridFieldPaginator:
@ -322,7 +319,4 @@ fr:
LOGOUT: 'Se déconnecter'
LOSTPASSWORDHEADER: 'Mot de passe oublié'
NOTEPAGESECURED: 'Cette page est sécurisée. Entrez vos identifiants ci-dessous et vous pourrez y avoir accès.'
NOTERESETLINKINVALID: '<p>Le lien de réinitialisation du mot de passe nest pas valide ou a expiré.</p><p>Vous pouvez en demander un nouveau <a href="{link1}">en suivant ce lien</a> ou changer de mot de passe après <a href="{link2}">connexion</a>.</p>'
NOTERESETPASSWORD: 'Entrez votre adresse email et nous vous enverrons un lien pour modifier votre mot de passe'
PASSWORDSENTHEADER: "Lien de réinitialisation de mot de passe envoyé à «\_{email}\_»"
PASSWORDSENTTEXT: "Merci\_! Un lien de réinitialisation vient dêtre envoyé à «\_{email}\_», à condition que cette adresse existe."

View File

@ -167,7 +167,4 @@ id:
LOGIN: Masuk
LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa'
NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.'
NOTERESETLINKINVALID: '<p>Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.</p><p>Anda dapat meminta yang baru <a href="{link1}">di sini</a> atau mengganti kata kunci setelah Anda <a href="{link2}">masuk</a>.</p>'
NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci'
PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}'''
PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.'

View File

@ -166,7 +166,4 @@ id_ID:
LOGIN: Masuk
LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa'
NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.'
NOTERESETLINKINVALID: '<p>Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.</p><p>Anda dapat meminta yang baru <a href="{link1}">di sini</a> atau mengganti kata kunci setelah Anda <a href="{link2}">masuk</a>.</p>'
NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci'
PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}'''
PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.'

View File

@ -95,6 +95,8 @@ it:
DeletePermissionsFailure: 'Non hai i permessi per eliminare'
Deleted: 'Eliminato {type} {name}'
Save: Salva
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Non è possibile rimuovere te stesso da questo gruppo, perderesti i diritti di admin'
SilverStripe\Forms\GridField\GridFieldPaginator:
OF: di
Page: Pagina
@ -195,6 +197,7 @@ it:
many_many_Members: Membri
SilverStripe\Security\LoginAttempt:
Email: 'Indirizzo e-mail'
EmailHashed: 'Indirizzo email (hash)'
IP: 'Indirizzo IP'
PLURALNAME: 'Tentativi d''accesso'
PLURALS:
@ -236,6 +239,7 @@ it:
SUBJECTPASSWORDCHANGED: 'La tua password è stata cambiata'
SUBJECTPASSWORDRESET: 'Link per azzerare la tua password'
SURNAME: Cognome
VALIDATIONADMINLOSTACCESS: 'Non è possibile rimuovere tutti i gruppi admin dal tuo profilo'
ValidationIdentifierFailed: 'Non posso sovrascrivere l''utente esistente #{id} con identificatore identico ({name} = {value}))'
WELCOMEBACK: 'Bentornato, {firstname}'
YOUROLDPASSWORD: 'La tua vecchia password'
@ -252,6 +256,8 @@ it:
SilverStripe\Security\MemberAuthenticator\MemberAuthenticator:
ERRORWRONGCRED: 'I dettagli forniti non sembrano corretti. Per favore riprovare.'
NoPassword: 'Manca la password per questo utente.'
SilverStripe\Security\MemberAuthenticator\MemberLoginForm:
AUTHENTICATORNAME: 'E-mail & Password'
SilverStripe\Security\MemberPassword:
PLURALNAME: 'Password utenti'
PLURALS:
@ -313,7 +319,4 @@ it:
LOGOUT: Scollegati
LOSTPASSWORDHEADER: 'Password smarrita'
NOTEPAGESECURED: 'La pagina è protetta. Inserisci le credenziali qui sotto per poter andare avanti.'
NOTERESETLINKINVALID: '<p>Il link per azzerare la password non è valido o è scaduto.</p><p>Puoi richiederne uno nuovo <a href="{link1}">qui</a> o cambiare la tua password dopo che ti sei <a href="{link2}">connesso</a>.</p>'
NOTERESETPASSWORD: 'Inserisci il tuo indirizzo e-mail e ti verrà inviato un link per poter azzerare la tua password.'
PASSWORDSENTHEADER: 'Link per azzeramento della password inviato a ''{email}'''
PASSWORDSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato a ''{email}'', fornito un account esistente per questo indirizzo e-mail.'

View File

@ -146,7 +146,4 @@ ja:
ERRORPASSWORDPERMISSION: パスワードを変更する為に、ログインしなければなりません!
LOGIN: ログイン
NOTEPAGESECURED: このページはセキュリティで保護されております証明書キーを下記に入力してください。こちらからすぐに送信します
NOTERESETLINKINVALID: '<p>パスワードのリセットリンクは有効でないか期限切れです。</p><p> 新しいパスワードを要求することができます <a href="{link1}"> ここ </a> もしくはパスワードを変更することができます <a href="{link2}"> ログインした後 </a>.</p>'
NOTERESETPASSWORD: メールアドレスを入力してください、パスワードをリセットするURLを送信致します
PASSWORDSENTHEADER: 'パスワードリセットリンクは ''{email}'' に送信されました'
PASSWORDSENTTEXT: 'ありがとうございました! リセットリンクは、''{email}'' に、このアカウントが存在することを前提として送信されました。'

View File

@ -167,7 +167,4 @@ lt:
LOGIN: Prisijungti
LOSTPASSWORDHEADER: 'Slaptažodžio atstatymas'
NOTEPAGESECURED: 'Šis puslapis yra apsaugotas. Įveskite savo duomenis į žemiau esančius laukelius.'
NOTERESETLINKINVALID: '<p>Neteisinga arba negaliojanti slaptažodžio atstatymo nuoroda.</p><p>Galite atsisiųsti naują <a href="{link1}">čia</a> arba pasikeisti slaptažodį po to, kai <a href="{link2}">prisijungsite</a>.</p>'
NOTERESETPASSWORD: 'Įveskite savo e. pašto adresą ir atsiųsime slaptažodžio atstatymui skirtą nuorodą'
PASSWORDSENTHEADER: 'Slaptažodžio atstatymo nuoroda nusiųsta į ''{email}'''
PASSWORDSENTTEXT: 'Atstatymo nuoroda nusiųsta į ''{email}'''

View File

@ -149,7 +149,4 @@ mi:
LOGIN: Takiuru
LOSTPASSWORDHEADER: 'Kupuhipa Ngaro'
NOTEPAGESECURED: 'Kua ngita tēnā whārangi. Tāurua ō taipitoptio tuakiri ki raro, ā, mā mātou koe e tuku kia haere tonu.'
NOTERESETLINKINVALID: '<p>He muhu, kua mōnehu rānei te hono tautuhi kupuhipa anō.</p><p>Ka taea te tono i te mea hōu<a href="{link1}">i konei</a> ka huri rānei i tō kupuhipa ā muri i tō<a href="{link2}">takiuru</a>.</p>'
NOTERESETPASSWORD: 'Tāurua tō wāhitau īmēra, mā mātou e tuku tētahi hono ki a koe e taea ai te tautuhi anō i tō kupuhipa'
PASSWORDSENTHEADER: 'I tukuna he hono tautuhi kupuhipa anō ki ''{email}'''
PASSWORDSENTTEXT: 'Kia ora! Kua tukuna he hono tautuhi anō ki ''{email}'',engari rā kei te tīariari he pūkete mō taua wāhitau īmēra.'

View File

@ -152,7 +152,4 @@ nb:
LOGIN: 'Logg inn'
LOSTPASSWORDHEADER: 'Mistet passord'
NOTEPAGESECURED: 'Den siden er sikret. Skriv inn gyldig innloggingsinfo så kommer du inn.'
NOTERESETLINKINVALID: '<p>Lenken for å nullstille passordet er ugyldig eller utgått.</p><p>Du kan kreve en ny <a href="{link1}">her</a> eller endre passordet etter at du har <a href="{link2}">logget inn</a>.</p>'
NOTERESETPASSWORD: 'Skriv inn epostadressen din og vi vil sende deg en lenke som nullstiller passordet.'
PASSWORDSENTHEADER: 'Lenke for nullstilling av passord ble sendt til ''{email}'''
PASSWORDSENTTEXT: 'Takk! En lenke for å lage nytt passord er sendt til ''{email}'', forutsatt at det eksisterer en konto for denne epostadressen.'

View File

@ -1,4 +1,26 @@
nl:
SilverStripe\Admin\LeftAndMain:
VersionUnknown: onbekend
SilverStripe\AssetAdmin\Forms\UploadField:
Dimensions: Afmetingen
EDIT: Bewerken
EDITINFO: 'Bewerk dit bestand'
REMOVE: Verwijder
SilverStripe\Control\ChangePasswordEmail_ss:
CHANGEPASSWORDFOREMAIL: 'Het wachtwoord voor het account met e-mailadres {email} is aangepast. Indien u uw wachtwoord niet heeft aangepast kunt u dat doen met onderstaande link.'
CHANGEPASSWORDTEXT1: 'U heeft het wachtwoord veranderd voor'
CHANGEPASSWORDTEXT3: 'Wachtwoord veranderen'
HELLO: Hallo
SilverStripe\Control\Email\ForgotPasswordEmail_ss:
HELLO: Hallo
TEXT1: 'Hier is uw'
TEXT2: 'link om uw wachtwoord opnieuw aan te maken'
TEXT3: voor
SilverStripe\Control\RequestProcessor:
INVALID_REQUEST: 'Fout bij verwerken'
REQUEST_ABORTED: 'Fout bij verwerken (geannuleerd)'
SilverStripe\Core\Manifest\VersionProvider:
VERSIONUNKNOWN: Onbekend
SilverStripe\Forms\CheckboxField:
NOANSWER: Nee
YESANSWER: Ja
@ -8,6 +30,8 @@ nl:
ATLEAST: 'Een wachtwoord moet tenminste {min} karakters hebben.'
BETWEEN: 'Een wachtwoord moet tussen de {min} en {max} karakters hebben'
CURRENT_PASSWORD_ERROR: 'Het wachtwoord dat u heeft ingevoerd is niet juist.'
CURRENT_PASSWORD_MISSING: 'Voer uw huidige wachtwoord in.'
LOGGED_IN_ERROR: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!'
MAXIMUM: 'Een wachtwoord mag maximaal {max} karakters hebben.'
SHOWONCLICKTITLE: 'Verander wachtwoord'
SilverStripe\Forms\CurrencyField:
@ -16,12 +40,20 @@ nl:
VALIDDATEFORMAT2: 'Vul een geldig datumformaat in ({format})'
VALIDDATEMAXDATE: 'De datum moet ouder of gelijk zijn aan de maximale datum ({date})'
VALIDDATEMINDATE: 'De datum moet nieuwer of gelijk zijn aan de minimale datum ({date})'
SilverStripe\Forms\DatetimeField:
VALIDDATEMAXDATETIME: 'De datum moet ouder of gelijk zijn aan de maximale datum ({datetime})'
VALIDDATETIMEFORMAT: 'Vul een geldige datum in ({format})'
VALIDDATETIMEMINDATE: 'De datum moet nieuwer of gelijk zijn aan de minimale datum ({datetime})'
SilverStripe\Forms\DropdownField:
CHOOSE: (Kies)
CHOOSE_MODEL: '(Selecteer {name})'
SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.'
SilverStripe\Forms\EmailField:
VALIDATION: 'Gelieve een e-mailadres in te voeren.'
SilverStripe\Forms\FileUploadReceiver:
FIELDNOTSET: 'Bestandsinformatie niet gevonden'
SilverStripe\Forms\Form:
BAD_METHOD: 'Dit formulier moet middels {method} verzonden worden'
CSRF_EXPIRED_MESSAGE: 'Uw sessie is verlopen. Verzend het formulier opnieuw.'
CSRF_FAILED_MESSAGE: 'Er lijkt een technisch probleem te zijn. Klik op de knop terug, vernieuw uw browser, en probeer het opnieuw.'
VALIDATIONPASSWORDSDONTMATCH: 'Wachtwoorden komen niet overeen'
@ -30,7 +62,10 @@ nl:
VALIDATOR: Validator
VALIDCURRENCY: 'Vul een geldige munteenheid in'
SilverStripe\Forms\FormField:
EXAMPLE: 'bijv. {format}'
NONE: geen
SilverStripe\Forms\FormScaffolder:
TABMAIN: Hoofdgedeelte
SilverStripe\Forms\GridField\GridField:
Add: '{name} toevoegen'
CSVEXPORT: 'Exporteer naar CSV'
@ -49,6 +84,7 @@ nl:
RelationSearch: 'Zoek relatie'
ResetFilter: Resetten
SilverStripe\Forms\GridField\GridFieldDeleteAction:
DELETE_DESCRIPTION: Verwijder
Delete: Verwijder
DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen'
EditPermissionsFailure: 'Geen toelating om te ontkoppelen'
@ -60,27 +96,74 @@ nl:
DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen'
Deleted: '{type} {name} verwijderd'
Save: Opslaan
SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: Edit
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'U kunt uzelf niet verwijderen van deze groep, omdat u dan geen admin-rechten meer heeft.'
SilverStripe\Forms\GridField\GridFieldPaginator:
OF: van
Page: Pagina
View: Bekijk
SilverStripe\Forms\GridField\GridFieldVersionedState:
ADDEDTODRAFTHELP: 'Item is nog niet gepubliceerd'
ADDEDTODRAFTSHORT: Concept
ARCHIVEDPAGEHELP: 'Het item is verwijderd van de concept- en de live site'
ARCHIVEDPAGESHORT: Gearchiveerd
MODIFIEDONDRAFTHELP: 'Item heeft wijzigingen die nog niet gepubliceerd zijn'
MODIFIEDONDRAFTSHORT: Aangepast
ONLIVEONLYSHORT: 'Alleen op de live site'
ONLIVEONLYSHORTHELP: 'Item is gepubliceerd, maar verwijderd van de concept site'
SilverStripe\Forms\MoneyField:
FIELDLABELAMOUNT: Aantal
FIELDLABELCURRENCY: Munteenheid
INVALID_CURRENCY: 'Valuta {currency} is niet toegestaan'
SilverStripe\Forms\MultiSelectField:
SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.'
SilverStripe\Forms\NullableField:
IsNullLabel: 'Is null'
SilverStripe\Forms\NumericField:
VALIDATION: '''{value}'' is geen getal, enkel getallen worden door dit veld geaccepteerd'
SilverStripe\Forms\TimeField:
VALIDATEFORMAT: 'Vul een geldig datumformaat in ({format})'
SilverStripe\ORM\DataObject:
PLURALNAME: 'Data objecten'
PLURALS:
one: 'Data object'
other: '{count} Data objecten'
SINGULARNAME: 'Data object'
SilverStripe\ORM\FieldType\DBBoolean:
ANY: Elke
NOANSWER: Nee
YESANSWER: Ja
SilverStripe\ORM\FieldType\DBDate:
DAYS_SHORT_PLURALS:
one: '{count} dag'
other: '{count} dagen'
HOURS_SHORT_PLURALS:
one: '{count} uur'
other: '{count} uren'
LessThanMinuteAgo: 'minder dan één minuut'
MINUTES_SHORT_PLURALS:
one: '{count} minuut'
other: '{count} minuten'
MONTHS_SHORT_PLURALS:
one: '{count} maand'
other: '{count} maanden'
SECONDS_SHORT_PLURALS:
one: '{count} seconde'
other: '{count} seconden'
TIMEDIFFAGO: '{difference} geleden'
TIMEDIFFIN: 'in {difference}'
YEARS_SHORT_PLURALS:
one: '{count} jaar'
other: '{count} jaren'
SilverStripe\ORM\FieldType\DBEnum:
ANY: Elke
SilverStripe\ORM\Hierarchy:
LIMITED_TITLE: 'Teveel onderliggende items ({count})'
SilverStripe\ORM\Hierarchy\Hierarchy:
InfiniteLoopNotAllowed: 'Oneindige lus gevonden in "{type}" hiërarchie. Wijzig het hogere niveau om dit op te lossen'
LIMITED_TITLE: 'Teveel onderliggende items ({count})'
SilverStripe\ORM\ValidationException:
DEFAULT_ERROR: Validatiefout
SilverStripe\Security\BasicAuth:
@ -91,34 +174,60 @@ nl:
PASSWORDEXPIRED: '<p>Uw wachtwoord is verlopen. <a target="_top" href="{link}">Kies een nieuw wachtwoord.</a></p>'
SilverStripe\Security\CMSSecurity:
INVALIDUSER: '<p>Ongeldige gebruiker <a target="_top" href="{link}">Log hier opnieuw in</a> om verder te gaan.</p>'
LOGIN_MESSAGE: 'Sessie is verlopen'
LOGIN_TITLE: '<p>U kunt verder met wat u aan het doen was, door opnieuw in te loggen.</p>'
SUCCESS: Succes
SUCCESSCONTENT: '<p>U bent ingelogd. <a target="_top" href="{link}">Klik hier</a> als u niet automatisch wordt doorgestuurd.</p>'
SUCCESS_TITLE: 'Inloggen is gelukt'
SilverStripe\Security\DefaultAdminService:
DefaultAdminFirstname: 'Standaard Beheerder'
SilverStripe\Security\Group:
AddRole: 'Voeg een rol toe aan deze groep'
Code: 'Groep code'
DefaultGroupTitleAdministrators: Beheerders
DefaultGroupTitleContentAuthors: 'Inhoud Auteurs'
Description: 'Omschrijving '
GROUPNAME: 'Groep naam'
GroupReminder: 'Als u de bovenliggende groep selecteert, neemt deze groep alle rollen over'
HierarchyPermsError: 'U moet (ADMIN) rechten hebben om de bovenliggende groep "{group}" toe te kennen'
Locked: 'Gesloten?'
MEMBERS: Leden
NEWGROUP: 'Nieuwe groep'
NoRoles: 'Geen rollen gevonden'
PERMISSIONS: Rechten
PLURALNAME: Groepen
PLURALS:
one: 'Een groep'
other: '{count} groepen'
Parent: 'Bovenliggende groep'
ROLES: Rollen
ROLESDESCRIPTION: 'Rollen zijn logische groeperingen van rechten die in het Rollen tabblad gewijzigd kunnen worden.<br />Rollen worden automatisch overgenomen van bovenliggende groepen.'
RolesAddEditLink: 'Rollen beheren'
SINGULARNAME: Groep
Sort: Sorteer-richting
has_many_Permissions: Rechten
many_many_Members: Leden
SilverStripe\Security\LoginAttempt:
Email: 'E-mailadres '
EmailHashed: 'E-mailadres (versleuteld)'
IP: 'IP adres'
PLURALNAME: Inlogpogingen
PLURALS:
one: 'Een inlogpoging'
other: '{count} inlogpogingen'
SINGULARNAME: Inlogpogingen
Status: Status
SilverStripe\Security\Member:
ADDGROUP: 'Groep toevoegen'
BUTTONCHANGEPASSWORD: 'Wachtwoord veranderen'
BUTTONLOGIN: Inloggen
BUTTONLOGINOTHER: 'Als iemand anders inloggen'
BUTTONLOGOUT: Uitloggen
BUTTONLOSTPASSWORD: 'Ik ben mijn wachtwoord vergeten'
CONFIRMNEWPASSWORD: 'Bevestig het nieuwe wachtwoord'
CONFIRMPASSWORD: 'Bevestig wachtwoord'
CURRENT_PASSWORD: 'Huidige wachtwoord'
EDIT_PASSWORD: 'Nieuw wachtwoord'
EMAIL: E-mail
EMPTYNEWPASSWORD: 'Het nieuwe wachtwoord mag niet leeg zijn, probeer opnieuw'
ENTEREMAIL: 'Typ uw e-mailadres om een link te ontvangen waarmee u uw wachtwoord kunt resetten.'
@ -128,13 +237,21 @@ nl:
ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.'
FIRSTNAME: Voornaam
INTERFACELANG: 'Interface taal'
KEEPMESIGNEDIN: 'Houd mij ingelogd'
LOGGEDINAS: 'U bent ingelogd als {name}.'
NEWPASSWORD: 'Nieuw wachtwoord'
PASSWORD: Wachtwoord
PASSWORDEXPIRED: 'Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.'
PLURALNAME: Leden
PLURALS:
one: 'Een lid'
other: '{count} leden'
REMEMBERME: 'Onthoud mij voor volgende keer? (voor {count} dagen op dit apparaat)'
SINGULARNAME: Lid
SUBJECTPASSWORDCHANGED: 'Uw wachtwoord is veranderd'
SUBJECTPASSWORDRESET: 'Link om uw wachtwoord opnieuw aan te maken'
SURNAME: Achternaam
VALIDATIONADMINLOSTACCESS: 'Niet mogelijk om alle admin-groepen te verwijderen van uw profiel'
ValidationIdentifierFailed: 'Een bestaande gebruiker #{id} kan niet dezelfde unieke velden hebben ({name} = {value}))'
WELCOMEBACK: 'Welkom terug, {firstname}'
YOUROLDPASSWORD: 'Uw oude wachtwoord'
@ -143,15 +260,38 @@ nl:
db_LockedOutUntil: 'Gesloten tot'
db_Password: Wachtwoord
db_PasswordExpiry: 'Wachtwoord vervaldatum'
SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm:
AUTHENTICATORNAME: Inlogformulier
BUTTONFORGOTPASSWORD: 'Wachtwoord vergeten'
BUTTONLOGIN: 'Opnieuw inloggen'
BUTTONLOGOUT: Uitloggen
SilverStripe\Security\MemberAuthenticator\MemberAuthenticator:
ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.'
NoPassword: 'Er is geen wachtwoord voor deze gebruiker.'
SilverStripe\Security\MemberAuthenticator\MemberLoginForm:
AUTHENTICATORNAME: 'E-mail & wachtwoord'
SilverStripe\Security\MemberPassword:
PLURALNAME: Gebruikerswachtwoorden
PLURALS:
one: 'Een gebruikerswachtwoord'
other: '{count} Gebruikerswachtwoorden'
SINGULARNAME: Gebruikerswachtwoord
SilverStripe\Security\PasswordValidator:
LOWCHARSTRENGTH: 'Maak a.u.b. uw wachtwoord sterker door enkele van de volgende karakters te gebruiken: {chars}'
PREVPASSWORD: 'U heeft dit wachtwoord in het verleden al gebruikt, kies a.u.b. een nieuw wachtwoord.'
TOOSHORT: 'Het wachtwoord is te kort, het moet minimaal {minimum} karakters hebben'
SilverStripe\Security\Permission:
AdminGroup: Beheerder
CMS_ACCESS_CATEGORY: 'CMS toegang'
CONTENT_CATEGORY: Inhoudsrechten
FULLADMINRIGHTS: 'Volledige admin rechten'
FULLADMINRIGHTS_HELP: 'Impliceert en overstemt alle andere toegewezen rechten.'
PERMISSIONS_CATEGORY: 'Rollen en toegangsrechten'
PLURALNAME: Rechten
PLURALS:
one: Machtiging
other: '{count} rechten'
SINGULARNAME: Machtiging
UserPermissionsIntro: 'Groepen aan deze gebruiker toewijzen zullen diens permissies aanpassen. Zie de sectie Groepen voor meer informatie over machtigingen voor afzonderlijke groepen.'
SilverStripe\Security\PermissionCheckboxSetField:
AssignedTo: 'toegewezen aan "{title}"'
@ -161,18 +301,34 @@ nl:
SilverStripe\Security\PermissionRole:
OnlyAdminCanApply: 'Alleen admin kan doorvoeren'
PLURALNAME: Rollen
PLURALS:
one: 'Een rol'
other: '{count} rollen'
SINGULARNAME: Rol
Title: Titel
SilverStripe\Security\PermissionRoleCode:
PLURALNAME: 'Permissie codes'
PLURALS:
one: 'Een permissiecode'
other: '{count} permissiecodes'
PermsError: 'U moet (ADMIN) rechten hebben om de code "{code}" toe te kennen'
SINGULARNAME: Permissiecode
SilverStripe\Security\RememberLoginHash:
PLURALNAME: 'Versleutelde logins'
PLURALS:
one: 'Een versleutelde login'
other: '{count} versleutelde logins'
SINGULARNAME: 'Versleutelde login'
SilverStripe\Security\Security:
ALREADYLOGGEDIN: 'U hebt geen toegang tot deze pagina. Als u een andere account met de nodige rechten hebt, kan u hieronder opnieuw inloggen.'
BUTTONSEND: 'Nieuw wachtwoord aanmaken'
CHANGEPASSWORDBELOW: 'U kunt uw wachtwoord hieronder veranderen.'
CHANGEPASSWORDHEADER: 'Verander uw wachtwoord'
CONFIRMLOGOUT: 'Klik op onderstaande knop om uit te loggen.'
ENTERNEWPASSWORD: 'Voer een nieuw wachtwoord in.'
ERRORPASSWORDPERMISSION: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!'
LOGIN: 'Meld aan'
LOGOUT: Uitloggen
LOSTPASSWORDHEADER: 'Wachtwoord vergeten'
NOTEPAGESECURED: 'Deze pagina is beveiligd. Voer uw gegevens in en u wordt automatisch doorgestuurd.'
NOTERESETLINKINVALID: '<p>De link om uw wachtwoord te kunnen wijzigen is niet meer geldig.</p><p>U kunt <a href="{link1}">een nieuwe link aanvragen</a> of uw wachtwoord aanpassen door <a href="{link2}">in te loggen</a>.</p>'

View File

@ -84,7 +84,6 @@ pl:
RelationSearch: 'Wyszukiwanie powiązań'
ResetFilter: Resetuj
SilverStripe\Forms\GridField\GridFieldDeleteAction:
DELETE_DESCRIPTION: Usuń
Delete: Usuń
DeletePermissionsFailure: 'Brak uprawnień do usuwania'
EditPermissionsFailure: 'Nie masz uprawnień, aby odłączyć rekord'
@ -96,8 +95,6 @@ pl:
DeletePermissionsFailure: 'Brak uprawnień do usuwania'
Deleted: 'Usunięto {type} {name}'
Save: Zapisz
SilverStripe\Forms\GridField\GridFieldEditButton_ss:
EDIT: Edytuj
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Nie możesz usunąć siebie z tej grupy, stracone zostałby prawa administratora'
SilverStripe\Forms\GridField\GridFieldPaginator:
@ -352,7 +349,4 @@ pl:
LOGOUT: 'Wyloguj się'
LOSTPASSWORDHEADER: 'Nie pamiętam hasła'
NOTEPAGESECURED: 'Ta strona jest zabezpieczona. Wpisz swoje dane a my wyślemy Ci potwierdzenie niebawem'
NOTERESETLINKINVALID: '<p>Link resetujący hasło wygasł lub jest nieprawidłowy.</p><p>Możesz poprosić o nowy <a href="{link1}">tutaj</a> lub zmień swoje hasło po <a href="{link2}">zalogowaniu się</a>.</p>'
NOTERESETPASSWORD: 'Wpisz adres e-mail, na który mamy wysłać link gdzie możesz zresetować swoje hasło'
PASSWORDSENTHEADER: 'Link resetujący hasła został wysłany do ''{email}'''
PASSWORDSENTTEXT: 'Dziękujemy! Link resetujący hasło został wysłany do ''{email}'', o ile konto użytkownika dla takiego e-maila istnieje.'

View File

@ -339,7 +339,4 @@ ru:
LOGOUT: Выйти
LOSTPASSWORDHEADER: 'Восстановление пароля'
NOTEPAGESECURED: 'Эта страница защищена. Пожалуйста, введите свои учетные данные для входа.'
NOTERESETLINKINVALID: '<p>Неверная ссылка переустановки пароля или время действия ссылки истекло.</p><p>Вы можете повторно запросить ссылку, щелкнув <a href="{link1}">здесь</a>, или поменять пароль, <a href="{link2}">войдя в систему</a>.</p> '
NOTERESETPASSWORD: 'Введите Ваш адрес email, и Вам будет отправлена ссылка, по которой Вы сможете переустановить свой пароль'
PASSWORDSENTHEADER: 'Ссылка для переустановки пароля выслана на ''{email}'''
PASSWORDSENTTEXT: 'Ссылка переустановки пароля была выслана на адрес ''{email}'' (письмо дойдет до получателя только в том случае, если аккаунт с таким электронным адресом действительно зарегистрирован).'

View File

@ -228,7 +228,4 @@ sk:
LOGIN: Prihlásiť
LOSTPASSWORDHEADER: 'Zabudnuté heslo'
NOTEPAGESECURED: 'Táto stránka je zabezpečená. Zadajte svoje prihlasovacie údaje a my Vám zároveň pošleme práva.'
NOTERESETLINKINVALID: '<p>Odkaz na resetovanie hesla nie je platný alebo je vypršala jeho platnosť.</p><p>Môžete požiadať o nový <a href="{link1}">tu</a> alebo zmeňte svoje heslo po <a href="{link2}">prihlásení</a>.</p>'
NOTERESETPASSWORD: 'Zadajte svoju e-mailovú adresu a my Vám pošleme odkaz na resetovanie hesla'
PASSWORDSENTHEADER: 'Odkaz na resetovanie hesla bol odoslaný na ''{email}'''
PASSWORDSENTTEXT: 'Ďakujeme! Resetovací odkaz bol odoslaný na ''''{email}'''', pokiaľ účet existuje pre túto emailovú adresu.'

View File

@ -135,7 +135,4 @@ sl:
LOGIN: Prijava
LOSTPASSWORDHEADER: 'Izgubljeno geslo'
NOTEPAGESECURED: 'Stran je zaščitena. Da bi lahko nadaljevali, vpišite svoje podatke.'
NOTERESETLINKINVALID: '<p>Povezava za ponastavitev gesla je napačna ali pa je njena veljavnost potekla.</p><p><a href="{link1}">Tukaj</a> lahko zaprosite za novo povezavo or pa zamenjate geslo, ko <a href="{link2}">se prijavite v sistem</a>.</p>'
NOTERESETPASSWORD: 'Vpišite e-naslov, na katerega vam bomo poslali povezavo za ponastavitev gesla'
PASSWORDSENTHEADER: 'Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}''.'
PASSWORDSENTTEXT: 'Hvala! Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}'', ki je naveden kot e-naslov vašega računa. '

View File

@ -151,7 +151,4 @@ sr:
ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!'
LOGIN: Пријављивање
NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.'
NOTERESETLINKINVALID: '<p>Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.</p><p>Можете да захтевате нови <a href="{link1}">овде</a> или да промените Вашу лозинку након што се <a href="{link2}">пријавите</a>.</p>'
NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку'
PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}'''
PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.'

View File

@ -150,7 +150,4 @@ sr@latin:
ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!'
LOGIN: Prijavljivanje
NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.'
NOTERESETLINKINVALID: '<p>Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.</p><p>Možete da zahtevate novi <a href="{link1}">ovde</a> ili da promenite Vašu lozinku nakon što se <a href="{link2}">prijavite</a>.</p>'
NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku'
PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}'''
PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.'

View File

@ -150,7 +150,4 @@ sr_RS:
ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!'
LOGIN: Пријављивање
NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.'
NOTERESETLINKINVALID: '<p>Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.</p><p>Можете да захтевате нови <a href="{link1}">овде</a> или да промените Вашу лозинку након што се <a href="{link2}">пријавите</a>.</p>'
NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку'
PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}'''
PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.'

View File

@ -151,7 +151,4 @@ sr_RS@latin:
ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!'
LOGIN: Prijavljivanje
NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.'
NOTERESETLINKINVALID: '<p>Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.</p><p>Možete da zahtevate novi <a href="{link1}">ovde</a> ili da promenite Vašu lozinku nakon što se <a href="{link2}">prijavite</a>.</p>'
NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku'
PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}'''
PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.'

View File

@ -93,7 +93,10 @@ sv:
DeletePermissionsFailure: 'Rättighet för att radera saknas'
Deleted: 'Raderade {type} {name}'
Save: Spara
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Du kan inte radera dig själv från den här gruppen, då du då kommer att förlora dina admin-rättigheter'
SilverStripe\Forms\GridField\GridFieldPaginator:
OF: av
Page: Sida
View: Visa
SilverStripe\Forms\MoneyField:
@ -108,6 +111,12 @@ sv:
VALIDATION: '''{value}'' är inget nummer, bara siffror (utan mellanslag) kan accepteras för det här fältet'
SilverStripe\Forms\TimeField:
VALIDATEFORMAT: 'Var god att ange tid i ett giltigt format ({format})'
SilverStripe\ORM\DataObject:
PLURALNAME: Dataobjekt
PLURALS:
one: 'Ett dataobjekt'
other: '{count} Dataobjekt'
SINGULARNAME: Dataobjekt
SilverStripe\ORM\FieldType\DBBoolean:
ANY: 'Vilken som helst'
NOANSWER: Nej
@ -221,6 +230,7 @@ sv:
PLURALS:
one: 'En medlem'
other: '{count} medlemmar'
REMEMBERME: 'Kom ihåg mig nästa gång? (i {count} dagar på denna enhet)'
SINGULARNAME: Medlem
SUBJECTPASSWORDCHANGED: 'Ditt lösenord har ändrats'
SUBJECTPASSWORDRESET: 'Din återställningslänk'
@ -288,7 +298,4 @@ sv:
LOGOUT: 'Logga ut'
LOSTPASSWORDHEADER: 'Bortglömt lösenord'
NOTEPAGESECURED: 'Den här sidan är låst. Fyll i dina uppgifter nedan så skickar vi dig vidare.'
NOTERESETLINKINVALID: '<p>Återställningslänk för lösenord är felaktig eller för gammal.</p><p>Du kan begära en ny <a href="{link1}">här</a> eller ändra ditt lösenord när du <a href="{link2}">loggat in</a>.</p>'
NOTERESETPASSWORD: 'Ange din e-postadress så skickar vi en länk med vilken du kan återställa ditt lösenord'
PASSWORDSENTHEADER: 'Återställningslänk för lösenord har skickats till ''{email}'''
PASSWORDSENTTEXT: 'Tack en återställningslänk har skickats till ''{email}'', förutsatt att ett konto med den addressen finns.'

View File

@ -166,7 +166,4 @@ zh:
LOGIN: 登录
LOSTPASSWORDHEADER: 忘记密码
NOTEPAGESECURED: 该页面受安全保护。请在下面输入您的证书,然后我们会立即将您引导至该页面。
NOTERESETLINKINVALID: '<p>密码重设链接无效或已过期。</p><p>您可以在<a href="{link1}">这里</a> 要求一个新的或在<a href="{link2}">登录</a>后更改您的密码。</p>'
NOTERESETPASSWORD: 请输入您的电子邮件地址,然后我们会将一个链接发送给您,您可以用它来重设您的密码
PASSWORDSENTHEADER: '密码重设链接已发送至''{email}'''
PASSWORDSENTTEXT: '谢谢!复位链接已发送到 ''{email}'',假定此电子邮件地址存在一个帐户。'

View File

@ -1004,12 +1004,12 @@ class Director implements TemplateGlobalProvider
$request = self::currentRequest($request);
if ($request) {
return $request->isAjax();
} else {
return (
isset($_REQUEST['ajax']) ||
(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
);
}
return (
isset($_REQUEST['ajax']) ||
(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
);
}
/**

View File

@ -41,7 +41,7 @@ class HTTPApplication implements Application
*/
public function handle(HTTPRequest $request)
{
$flush = array_key_exists('flush', $request->getVars()) || strpos($request->getURL(), 'dev/build') === 0;
$flush = array_key_exists('flush', $request->getVars()) || ($request->getURL() === 'dev/build');
// Ensure boot is invoked
return $this->execute($request, function (HTTPRequest $request) {

View File

@ -0,0 +1,67 @@
<?php
namespace SilverStripe\Control\Middleware;
use SilverStripe\Assets\File;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Dev\Debug;
/**
* Secures requests by only allowing a whitelist of Host values
*/
class ExecMetricMiddleware implements HTTPMiddleware
{
/**
* @inheritdoc
*/
public function process(HTTPRequest $request, callable $delegate)
{
if (!$this->showMetric($request)) {
return $delegate($request);
}
$start = microtime(true);
try {
return $delegate($request);
} finally {
$end = microtime(true);
Debug::message(
sprintf(
"Execution time: %s, Peak memory usage: %s\n",
$this->formatExecutionTime($start, $end),
$this->formatPeakMemoryUsage()
),
false
);
}
}
private function showMetric(HTTPRequest $request)
{
return Director::isDev() && array_key_exists('execmetric', $request->getVars());
}
/**
* Convert the provided start and end time to a interval in secs.
* @param float $start
* @param float $end
* @return string
*/
private function formatExecutionTime($start, $end)
{
$diff = round($end - $start, 4);
return $diff . ' seconds';
}
/**
* Get the peak memory usage formatted has a string and a meaningful unit.
* @return string
*/
private function formatPeakMemoryUsage()
{
$bytes = memory_get_peak_usage(true);
return File::format_size($bytes);
}
}

View File

@ -9,46 +9,43 @@ use SilverStripe\Dev\Deprecation;
/**
* Handles all manipulation of the session.
*
* The static methods are used to manipulate the currently active controller's session.
* The instance methods are used to manipulate a particular session. There can be more than one of these created.
* An instance of a `Session` object can be retrieved via an `HTTPRequest` by calling the `getSession()` method.
*
* In order to support things like testing, the session is associated with a particular Controller. In normal usage,
* this is loaded from and saved to the regular PHP session, but for things like static-page-generation and
* unit-testing, you can create multiple Controllers, each with their own session.
*
* The instance object is basically just a way of manipulating a set of nested maps, and isn't specific to session
* data.
*
* <b>Saving Data</b>
*
* You can write a value to a users session from your PHP code using the static function {@link Session::set()}. You
* can add this line in any function or file you wish to save the value.
* Once you've retrieved a session instance, you can write a value to a users session using the function {@link Session::set()}.
*
* <code>
* Session::set('MyValue', 6);
* $request->getSession()->set('MyValue', 6);
* </code>
*
* Saves the value of "6" to the MyValue session data. You can also save arrays or serialized objects in session (but
* note there may be size restrictions as to how much you can save)
*
* <code>
*
* $session = $request->getSession();
*
* // save a variable
* $var = 1;
* Session::set('MyVar', $var);
* $session->set('MyVar', $var);
*
* // saves an array
* Session::set('MyArrayOfValues', array('1', '2', '3'));
* $session->set('MyArrayOfValues', array('1', '2', '3'));
*
* // saves an object (you'll have to unserialize it back)
* $object = new Object();
*
* Session::set('MyObject', serialize($object));
* $session->set('MyObject', serialize($object));
* </code>
*
* <b>Accessing Data</b>
*
* Once you have saved a value to the Session you can access it by using the {@link Session::get()} function.
* Like the {@link Session::set()} function you can use this anywhere in your PHP files.
* Note that session data isn't persisted in PHP's own session store (via $_SESSION)
* until {@link Session::save()} is called, which happens automatically at the end of a standard request
* through {@link SilverStripe\Control\Middleware\SessionMiddleware}.
@ -57,17 +54,18 @@ use SilverStripe\Dev\Deprecation;
*
* <code>
* public function bar() {
* $value = Session::get('MyValue'); // $value = 6
* $var = Session::get('MyVar'); // $var = 1
* $array = Session::get('MyArrayOfValues'); // $array = array(1,2,3)
* $object = Session::get('MyObject', unserialize($object)); // $object = Object()
* $session = $this->getRequest()->getSession();
* $value = $session->get('MyValue'); // $value = 6
* $var = $session->get('MyVar'); // $var = 1
* $array = $session->get('MyArrayOfValues'); // $array = array(1,2,3)
* $object = $session->get('MyObject', unserialize($object)); // $object = Object()
* }
* </code>
*
* You can also get all the values in the session at once. This is useful for debugging.
*
* <code>
* Session::get_all(); // returns an array of all the session values.
* $session->getAll(); // returns an array of all the session values.
* </code>
*
* <b>Clearing Data</b>
@ -76,17 +74,18 @@ use SilverStripe\Dev\Deprecation;
* to specifically remove it. To clear a value you can either delete 1 session value by the name that you saved it
*
* <code>
* Session::clear('MyValue'); // MyValue is no longer 6.
* $session->clear('MyValue'); // MyValue is no longer 6.
* </code>
*
* Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data
* including form and page comment information. None of this is vital but clear_all will clear everything.
* including form and page comment information. None of this is vital but `clearAll()` will clear everything.
*
* <code>
* Session::clear_all();
* $session->clearAll();
* </code>
*
* @see Cookie
* @see HTTPRequest
*/
class Session
{
@ -300,55 +299,52 @@ class Session
// If the session cookie is already set, then the session can be read even if headers_sent() = true
// This helps with edge-case such as debugging.
if (!session_id() && (!headers_sent() || !empty($_COOKIE[ini_get('session.name')]))) {
$data = [];
if (!session_id() && (!headers_sent() || $this->requestContainsSessionId($request))) {
if (!headers_sent()) {
session_set_cookie_params($timeout, $path, $domain ?: null, $secure, true);
session_set_cookie_params($timeout ?: 0, $path, $domain ?: null, $secure, true);
$limiter = $this->config()->get('sessionCacheLimiter');
if (isset($limiter)) {
session_cache_limiter($limiter);
}
// If headers are sent then we can't have a session_cache_limiter otherwise we'll get a warning
// Allow storing the session in a non standard location
if ($session_path) {
session_save_path($session_path);
}
// If we want a secure cookie for HTTPS, use a separate session name. This lets us have a
// separate (less secure) session for non-HTTPS requests
// if headers_sent() is true then it's best to throw the resulting error rather than risk
// a security hole.
if ($secure) {
session_name($this->config()->get('cookie_name_secure'));
}
session_start();
// Session start emits a cookie, but only if there's no existing session. If there is a session timeout
// tied to this request, make sure the session is held for the entire timeout by refreshing the cookie age.
if ($timeout && $this->requestContainsSessionId($request)) {
Cookie::set(session_name(), session_id(), $timeout / 86400, $path, $domain ?: null, $secure, true);
}
} else {
// If headers are sent then we can't have a session_cache_limiter otherwise we'll get a warning
session_cache_limiter(null);
}
// Allow storing the session in a non standard location
if ($session_path) {
session_save_path($session_path);
}
// If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a
// seperate (less secure) session for non-HTTPS requests. Note that if this causes problems
// if headers_sent() is true then it's best to throw the resulting error rather than risk
// a security hole.
if ($secure) {
session_name($this->config()->get('cookie_name_secure'));
}
session_start();
if (isset($_SESSION)) {
// Initialise data from session store if present
$data = $_SESSION;
// Merge in existing in-memory data, taking priority over session store data
$this->recursivelyApply((array)$this->data, $data);
} else {
// Use in-memory data if the session is lazy started
$data = $this->data;
}
$this->data = $data ?: [];
} else {
$this->data = [];
}
// Modify the timeout behaviour so it's the *inactive* time before the session expires.
// By default it's the total session lifetime
if ($timeout && !headers_sent()) {
Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain
: null, $secure, true);
}
// Save any modified session data back to the session store if present, otherwise initialise it to an array.
$this->data = $data;
$this->started = true;
}
@ -438,7 +434,7 @@ class Session
}
$var[] = $val;
$diffVar[sizeof($var)-1] = $val;
$diffVar[sizeof($var) - 1] = $val;
}
/**

View File

@ -133,15 +133,15 @@ class SimpleResourceURLGenerator implements ResourceURLGenerator
$exists = $resource->exists();
$absolutePath = $resource->getPath();
// Rewrite to resources with public directory
// Rewrite to _resources with public directory
if (Director::publicDir()) {
// All resources mapped directly to resources/
$relativePath = Path::join(ManifestFileFinder::RESOURCES_DIR, $relativePath);
// All resources mapped directly to _resources/
$relativePath = Path::join(RESOURCES_DIR, $relativePath);
} elseif (stripos($relativePath, ManifestFileFinder::VENDOR_DIR . DIRECTORY_SEPARATOR) === 0) {
// @todo Non-public dir support will be removed in 5.0, so remove this block there
// If there is no public folder, map to resources/ but trim leading vendor/ too (4.0 compat)
// If there is no public folder, map to _resources/ but trim leading vendor/ too (4.0 compat)
$relativePath = Path::join(
ManifestFileFinder::RESOURCES_DIR,
RESOURCES_DIR,
substr($relativePath, strlen(ManifestFileFinder::VENDOR_DIR))
);
}
@ -167,10 +167,10 @@ class SimpleResourceURLGenerator implements ResourceURLGenerator
$absolutePath = Path::join(Director::baseFolder(), $relativePath);
$exists = file_exists($absolutePath);
// Rewrite vendor/ to resources/ folder
// Rewrite vendor/ to _resources/ folder
if (stripos($relativePath, ManifestFileFinder::VENDOR_DIR . DIRECTORY_SEPARATOR) === 0) {
$relativePath = Path::join(
ManifestFileFinder::RESOURCES_DIR,
RESOURCES_DIR,
substr($relativePath, strlen(ManifestFileFinder::VENDOR_DIR))
);
}
@ -200,7 +200,7 @@ class SimpleResourceURLGenerator implements ResourceURLGenerator
/**
* Resolve a resource that may either exist in a public/ folder, or be exposed from the base path to
* public/resources/
* public/_resources/
*
* @param string $relativePath
* @return array List of [$exists, $absolutePath, $relativePath]
@ -217,11 +217,11 @@ class SimpleResourceURLGenerator implements ResourceURLGenerator
return [true, $publicPath, $relativePath];
}
// Fall back to private path (and assume expose will make this available to resources/)
// Fall back to private path (and assume expose will make this available to _resources/)
$privatePath = Path::join(Director::baseFolder(), $relativePath);
if (!$publicOnly && file_exists($privatePath)) {
// String is private but exposed to resources/, so rewrite to the symlinked base
$relativePath = Path::join(ManifestFileFinder::RESOURCES_DIR, $relativePath);
// String is private but exposed to _resources/, so rewrite to the symlinked base
$relativePath = Path::join(RESOURCES_DIR, $relativePath);
return [true, $privatePath, $relativePath];
}

View File

@ -70,9 +70,9 @@ class Convert
}
return $val;
} else {
return self::raw2att($val);
}
return self::raw2att($val);
}
/**
@ -93,16 +93,16 @@ class Convert
}
return $val;
} else {
return trim(
preg_replace(
'/_+/',
'_',
preg_replace('/[^a-zA-Z0-9\-_:.]+/', '_', $val)
),
'_'
);
}
return trim(
preg_replace(
'/_+/',
'_',
preg_replace('/[^a-zA-Z0-9\-_:.]+/', '_', $val)
),
'_'
);
}
/**
@ -119,9 +119,9 @@ class Convert
$val[$k] = self::raw2xml($v);
}
return $val;
} else {
return htmlspecialchars($val, ENT_QUOTES, 'UTF-8');
}
return htmlspecialchars($val, ENT_QUOTES, 'UTF-8');
}
/**
@ -137,14 +137,14 @@ class Convert
$val[$k] = self::raw2js($v);
}
return $val;
} else {
return str_replace(
// Intercepts some characters such as <, >, and & which can interfere
array("\\", '"', "\n", "\r", "'", "<", ">", "&"),
array("\\\\", '\"', '\n', '\r', "\\'", "\\x3c", "\\x3e", "\\x26"),
$val
);
}
return str_replace(
// Intercepts some characters such as <, >, and & which can interfere
array("\\", '"', "\n", "\r", "'", '<', '>', '&'),
array("\\\\", '\"', '\n', '\r', "\\'", "\\x3c", "\\x3e", "\\x26"),
$val
);
}
/**
@ -195,13 +195,13 @@ class Convert
$val[$k] = self::raw2sql($v, $quoted);
}
return $val;
} else {
if ($quoted) {
return DB::get_conn()->quoteString($val);
} else {
return DB::get_conn()->escapeString($val);
}
}
if ($quoted) {
return DB::get_conn()->quoteString($val);
}
return DB::get_conn()->escapeString($val);
}
/**
@ -233,14 +233,14 @@ class Convert
$val[$k] = self::xml2raw($v);
}
return $val;
} else {
// More complex text needs to use html2raw instead
if (strpos($val, '<') !== false) {
return self::html2raw($val);
} else {
return html_entity_decode($val, ENT_QUOTES, 'UTF-8');
}
}
// More complex text needs to use html2raw instead
if (strpos($val, '<') !== false) {
return self::html2raw($val);
}
return html_entity_decode($val, ENT_QUOTES, 'UTF-8');
}
/**
@ -332,7 +332,7 @@ class Convert
$xml = get_object_vars($xml);
}
if (is_array($xml)) {
if (count($xml) == 0) {
if (count($xml) === 0) {
return (string)$x;
} // for CDATA
$r = [];
@ -359,9 +359,9 @@ class Convert
{
if (preg_match('/^[a-z+]+\:\/\/[a-zA-Z0-9$-_.+?&=!*\'()%]+$/', $string)) {
return "<a style=\"white-space: nowrap\" href=\"$string\">$string</a>";
} else {
return $string;
}
return $string;
}
/**
@ -387,8 +387,8 @@ class Convert
$config = $defaultConfig;
}
$data = preg_replace("/<style([^A-Za-z0-9>][^>]*)?>.*?<\/style[^>]*>/is", "", $data);
$data = preg_replace("/<script([^A-Za-z0-9>][^>]*)?>.*?<\/script[^>]*>/is", "", $data);
$data = preg_replace("/<style([^A-Za-z0-9>][^>]*)?>.*?<\/style[^>]*>/is", '', $data);
$data = preg_replace("/<script([^A-Za-z0-9>][^>]*)?>.*?<\/script[^>]*>/is", '', $data);
if ($config['ReplaceBoldAsterisk']) {
$data = preg_replace('%<(strong|b)( [^>]*)?>|</(strong|b)>%i', '*', $data);
@ -412,7 +412,7 @@ class Convert
// Compress whitespace
if ($config['CompressWhitespace']) {
$data = preg_replace("/\s+/u", " ", $data);
$data = preg_replace("/\s+/u", ' ', $data);
}
// Parse newline tags
@ -421,9 +421,9 @@ class Convert
$data = preg_replace("/\s*<[Dd][Ii][Vv]([^A-Za-z0-9>][^>]*)?> */u", "\n\n", $data);
$data = preg_replace("/\n\n\n+/", "\n\n", $data);
$data = preg_replace("/<[Bb][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data);
$data = preg_replace("/<[Tt][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data);
$data = preg_replace("/<\/[Tt][Dd]([^A-Za-z0-9>][^>]*)?> */", " ", $data);
$data = preg_replace('/<[Bb][Rr]([^A-Za-z0-9>][^>]*)?> */', "\n", $data);
$data = preg_replace('/<[Tt][Rr]([^A-Za-z0-9>][^>]*)?> */', "\n", $data);
$data = preg_replace("/<\/[Tt][Dd]([^A-Za-z0-9>][^>]*)?> */", ' ', $data);
$data = preg_replace('/<\/p>/i', "\n\n", $data);
// Replace HTML entities
@ -564,24 +564,25 @@ class Convert
/**
* Turn a memory string, such as 512M into an actual number of bytes.
* Preserves integer values like "1024" or "-1"
*
* @param string $memString A memory limit string, such as "64M"
* @return float
* @return int
*/
public static function memstring2bytes($memString)
{
// Remove non-unit characters from the size
$unit = preg_replace('/[^bkmgtpezy]/i', '', $memString);
// Remove non-numeric characters from the size
$size = preg_replace('/[^0-9\.]/', '', $memString);
$size = preg_replace('/[^0-9\.\-]/', '', $memString);
if ($unit) {
// Find the position of the unit in the ordered string which is the power
// of magnitude to multiply a kilobyte by
return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
return (int)round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
}
return round($size);
return (int)round($size);
}
/**

View File

@ -279,6 +279,10 @@ trait CustomMethods
$methods = $this->findMethodsFromExtension($extension);
if ($methods) {
foreach ($methods as $method) {
if (!isset(self::$extra_methods[$class][$method])) {
continue;
}
$methodInfo = self::$extra_methods[$class][$method];
if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {

View File

@ -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

View File

@ -13,6 +13,7 @@ use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Environment;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\DataObject;
/**
* A simple injection manager that manages creating objects and injecting
@ -581,6 +582,14 @@ class Injector implements ContainerInterface
$constructorParams = $spec['constructor'];
}
// If we're dealing with a DataObject singleton without specific constructor params, pass through Singleton
// flag as second argument
if ((!$type || $type !== self::PROTOTYPE)
&& empty($constructorParams)
&& is_subclass_of($class, DataObject::class)) {
$constructorParams = array(null, true);
}
$factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
$object = $factory->create($class, $constructorParams);

View File

@ -22,7 +22,11 @@ class ManifestFileFinder extends FileFinder
const LANG_DIR = 'lang';
const TESTS_DIR = 'tests';
const VENDOR_DIR = 'vendor';
const RESOURCES_DIR = 'resources';
/**
* @deprecated 4.4.0:5.0.0 Use global `RESOURCES_DIR` instead.
*/
const RESOURCES_DIR = RESOURCES_DIR;
protected static $default_options = array(
'include_themes' => false,
@ -41,6 +45,11 @@ class ManifestFileFinder extends FileFinder
// Keep searching inside vendor
$inVendor = $this->isInsideVendor($basename, $pathname, $depth);
if ($inVendor) {
// Skip nested vendor folders (e.g. vendor/silverstripe/framework/vendor)
if ($depth == 4 && basename($pathname) === self::VENDOR_DIR) {
return false;
}
// Keep searching if we could have a subdir module
if ($depth < 3) {
return true;
@ -236,7 +245,7 @@ class ManifestFileFinder extends FileFinder
}
// Ignore these dirs in the root only
if ($depth === 1 && in_array($basename, [ASSETS_DIR, self::RESOURCES_DIR])) {
if ($depth === 1 && in_array($basename, [ASSETS_DIR, RESOURCES_DIR])) {
return true;
}

View File

@ -8,6 +8,10 @@ use Serializable;
use SilverStripe\Core\Path;
use SilverStripe\Dev\Deprecation;
/**
* Abstraction of a PHP Package. Can be used to retrieve information about SilverStripe modules, and other packages
* managed via composer, by reading their `composer.json` file.
*/
class Module implements Serializable
{
/**
@ -124,6 +128,20 @@ class Module implements Serializable
return basename($this->path);
}
/**
* Name of the resource directory where vendor resources should be exposed as defined by the `extra.resources-dir`
* key in the composer file. A blank string will will be returned if the key is undefined.
*
* Only applicable when reading the composer file for the main project.
* @return string
*/
public function getResourcesDir()
{
return isset($this->composerData['extra']['resources-dir'])
? $this->composerData['extra']['resources-dir']
: '';
}
/**
* Get base path for this module
*

View File

@ -49,7 +49,7 @@ class ModuleResource
/**
* Return the full filesystem path to this resource.
*
* Note: In the case that this resource is mapped to the `resources` folder, this will
* Note: In the case that this resource is mapped to the `_resources` folder, this will
* return the original rather than the copy / symlink.
*
* @return string Path with no trailing slash E.g. /var/www/module
@ -62,7 +62,7 @@ class ModuleResource
/**
* Get the path of this resource relative to the base path.
*
* Note: In the case that this resource is mapped to the `resources` folder, this will
* Note: In the case that this resource is mapped to the `_resources` folder, this will
* return the original rather than the copy / symlink.
*
* @return string Relative path (no leading /)
@ -81,7 +81,7 @@ class ModuleResource
* Public URL to this resource.
* Note: May be either absolute url, or root-relative url
*
* In the case that this resource is mapped to the `resources` folder this
* In the case that this resource is mapped to the `_resources` folder this
* will be the mapped url rather than the original path.
*
* @return string

View File

@ -0,0 +1,192 @@
<?php
namespace SilverStripe\Core\Startup;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Security\RandomGenerator;
/**
* Shared functionality for token-based authentication of potentially dangerous URLs or query
* string parameters
*
* @internal This class is designed specifically for use pre-startup and may change without warning
*/
abstract class AbstractConfirmationToken
{
/**
* @var HTTPRequest
*/
protected $request = null;
/**
* The validated and checked token for this parameter
*
* @var string|null A string value, or null if either not provided or invalid
*/
protected $token = null;
/**
* Given a list of token names, suppress all tokens that have not been validated, and
* return the non-validated token with the highest priority
*
* @param array $keys List of token keys in ascending priority (low to high)
* @param HTTPRequest $request
* @return static The token container for the unvalidated $key given with the highest priority
*/
public static function prepare_tokens($keys, HTTPRequest $request)
{
$target = null;
foreach ($keys as $key) {
$token = new static($key, $request);
// Validate this token
if ($token->reloadRequired() || $token->reloadRequiredIfError()) {
$token->suppress();
$target = $token;
}
}
return $target;
}
/**
* Generate a local filesystem path to store a token
*
* @param $token
* @return string
*/
protected function pathForToken($token)
{
return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token);
}
/**
* Generate a new random token and store it
*
* @return string Token name
*/
protected function genToken()
{
// Generate a new random token (as random as possible)
$rg = new RandomGenerator();
$token = $rg->randomToken('md5');
// Store a file in the session save path (safer than /tmp, as open_basedir might limit that)
file_put_contents($this->pathForToken($token), $token);
return $token;
}
/**
* Is the necessary token provided for this parameter?
* A value must be provided for the token
*
* @return bool
*/
public function tokenProvided()
{
return !empty($this->token);
}
/**
* Validate a token
*
* @param string $token
* @return boolean True if the token is valid
*/
protected function checkToken($token)
{
if (!$token) {
return false;
}
$file = $this->pathForToken($token);
$content = null;
if (file_exists($file)) {
$content = file_get_contents($file);
unlink($file);
}
return $content === $token;
}
/**
* Get redirect url, excluding querystring
*
* @return string
*/
public function currentURL()
{
return Controller::join_links(Director::baseURL(), $this->request->getURL(false));
}
/**
* Forces a reload of the request with the token included
*
* @return HTTPResponse
*/
public function reloadWithToken()
{
$location = $this->redirectURL();
$locationJS = Convert::raw2js($location);
$locationATT = Convert::raw2att($location);
$body = <<<HTML
<script>location.href='$locationJS';</script>
<noscript><meta http-equiv="refresh" content="0; url=$locationATT"></noscript>
You are being redirected. If you are not redirected soon, <a href="$locationATT">click here to continue</a>
HTML;
// Build response
$result = new HTTPResponse($body);
$result->redirect($location);
return $result;
}
/**
* Is this parameter requested without a valid token?
*
* @return bool True if the parameter is given without a valid token
*/
abstract public function reloadRequired();
/**
* Check if this token is provided either in the backurl, or directly,
* but without a token
*
* @return bool
*/
abstract public function reloadRequiredIfError();
/**
* Suppress the current parameter for the duration of this request
*/
abstract public function suppress();
/**
* Determine the querystring parameters to include
*
* @param bool $includeToken Include the token value?
* @return array List of querystring parameters, possibly including token parameter
*/
abstract public function params($includeToken = true);
/**
* @return string
*/
abstract public function getRedirectUrlBase();
/**
* @return array
*/
abstract public function getRedirectUrlParams();
/**
* Get redirection URL
*
* @return string
*/
abstract protected function redirectURL();
}

View File

@ -0,0 +1,179 @@
<?php
namespace SilverStripe\Core\Startup;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
/**
* A chain of confirmation tokens to be validated on each request. This allows the application to
* check multiple tokens at once without having to potentially redirect the user for each of them
*
* @internal This class is designed specifically for use pre-startup and may change without warning
*/
class ConfirmationTokenChain
{
/**
* @var array
*/
protected $tokens = [];
/**
* @param AbstractConfirmationToken $token
*/
public function pushToken(AbstractConfirmationToken $token)
{
$this->tokens[] = $token;
}
/**
* Collect all tokens that require a redirect
*
* @return \Generator
*/
protected function filteredTokens()
{
foreach ($this->tokens as $token) {
if ($token->reloadRequired() || $token->reloadRequiredIfError()) {
yield $token;
}
}
}
/**
* @return bool
*/
public function suppressionRequired()
{
foreach ($this->tokens as $token) {
if ($token->reloadRequired()) {
return true;
}
}
return false;
}
/**
* Suppress URLs & GET vars from tokens that require a redirect
*/
public function suppressTokens()
{
foreach ($this->filteredTokens() as $token) {
$token->suppress();
}
}
/**
* @return bool
*/
public function reloadRequired()
{
foreach ($this->tokens as $token) {
if ($token->reloadRequired()) {
return true;
}
}
return false;
}
/**
* @return bool
*/
public function reloadRequiredIfError()
{
foreach ($this->tokens as $token) {
if ($token->reloadRequiredIfError()) {
return true;
}
}
return false;
}
/**
* @param bool $includeToken
* @return array
*/
public function params($includeToken = true)
{
$params = [];
foreach ($this->tokens as $token) {
$params = array_merge($params, $token->params($includeToken));
}
return $params;
}
/**
* Fetch the URL we want to redirect to, excluding query string parameters. This may
* be the same URL (with a token to be added outside this method), or to a different
* URL if the current one has been suppressed
*
* @return string
*/
public function getRedirectUrlBase()
{
// URLConfirmationTokens may alter the URL to suppress the URL they're protecting,
// so we need to ensure they're inspected last and therefore take priority
$tokens = iterator_to_array($this->filteredTokens(), false);
usort($tokens, function ($a, $b) {
return ($a instanceof URLConfirmationToken) ? 1 : 0;
});
$urlBase = Director::baseURL();
foreach ($tokens as $token) {
$urlBase = $token->getRedirectUrlBase();
}
return $urlBase;
}
/**
* Collate GET vars from all token providers that need to apply a token
*
* @return array
*/
public function getRedirectUrlParams()
{
$params = $_GET;
unset($params['url']); // CLIRequestBuilder may add this
foreach ($this->filteredTokens() as $token) {
$params = array_merge($params, $token->params());
}
return $params;
}
/**
* @return string
*/
protected function redirectURL()
{
$params = http_build_query($this->getRedirectUrlParams());
return Controller::join_links($this->getRedirectUrlBase(), '?' . $params);
}
/**
* @return HTTPResponse
*/
public function reloadWithTokens()
{
$location = $this->redirectURL();
$locationJS = Convert::raw2js($location);
$locationATT = Convert::raw2att($location);
$body = <<<HTML
<script>location.href='$locationJS';</script>
<noscript><meta http-equiv="refresh" content="0; url=$locationATT"></noscript>
You are being redirected. If you are not redirected soon, <a href="$locationATT">click here to continue</a>
HTML;
// Build response
$result = new HTTPResponse($body);
$result->redirect($location);
return $result;
}
}

View File

@ -15,8 +15,7 @@ use Exception;
* $chain = new ErrorControlChain();
* $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute();
*
* WARNING: This class is experimental and designed specifically for use pre-startup.
* It will likely be heavily refactored before the release of 3.2
* @internal This class is designed specifically for use pre-startup and may change without warning
*/
class ErrorControlChain
{

View File

@ -12,6 +12,8 @@ use SilverStripe\Security\Security;
/**
* Decorates application bootstrapping with errorcontrolchain
*
* @internal This class is designed specifically for use pre-startup and may change without warning
*/
class ErrorControlChainMiddleware implements HTTPMiddleware
{
@ -30,27 +32,42 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
$this->application = $application;
}
/**
* @param HTTPRequest $request
* @return ConfirmationTokenChain
*/
protected function prepareConfirmationTokenChain(HTTPRequest $request)
{
$chain = new ConfirmationTokenChain();
$chain->pushToken(new URLConfirmationToken('dev/build', $request));
foreach (['isTest', 'isDev', 'flush'] as $parameter) {
$chain->pushToken(new ParameterConfirmationToken($parameter, $request));
}
return $chain;
}
public function process(HTTPRequest $request, callable $next)
{
$result = null;
// Prepare tokens and execute chain
$reloadToken = ParameterConfirmationToken::prepare_tokens(
['isTest', 'isDev', 'flush'],
$request
);
$chain = new ErrorControlChain();
$chain
->then(function () use ($request, $chain, $reloadToken, $next, &$result) {
// If no redirection is necessary then we can disable error supression
if (!$reloadToken) {
$chain->setSuppression(false);
$confirmationTokenChain = $this->prepareConfirmationTokenChain($request);
$errorControlChain = new ErrorControlChain();
$errorControlChain
->then(function () use ($request, $errorControlChain, $confirmationTokenChain, $next, &$result) {
if ($confirmationTokenChain->suppressionRequired()) {
$confirmationTokenChain->suppressTokens();
} else {
// If no redirection is necessary then we can disable error supression
$errorControlChain->setSuppression(false);
}
try {
// Check if a token is requesting a redirect
if ($reloadToken && $reloadToken->reloadRequired()) {
$result = $this->safeReloadWithToken($request, $reloadToken);
if ($confirmationTokenChain && $confirmationTokenChain->reloadRequired()) {
$result = $this->safeReloadWithTokens($request, $confirmationTokenChain);
} else {
// If no reload necessary, process application
$result = call_user_func($next, $request);
@ -60,10 +77,16 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
}
})
// Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway
->thenIfErrored(function () use ($reloadToken) {
if ($reloadToken && $reloadToken->reloadRequiredIfError()) {
$result = $reloadToken->reloadWithToken();
$result->output();
->thenIfErrored(function () use ($confirmationTokenChain) {
if ($confirmationTokenChain && $confirmationTokenChain->reloadRequiredIfError()) {
try {
// Reload requires manual boot
$this->getApplication()->getKernel()->boot(false);
} finally {
// Given we're in an error state here, try to continue even if the kernel boot fails
$result = $confirmationTokenChain->reloadWithTokens();
$result->output();
}
}
})
->execute();
@ -75,21 +98,21 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
* or authentication is impossible.
*
* @param HTTPRequest $request
* @param ParameterConfirmationToken $reloadToken
* @param ConfirmationTokenChain $confirmationTokenChain
* @return HTTPResponse
*/
protected function safeReloadWithToken(HTTPRequest $request, $reloadToken)
protected function safeReloadWithTokens(HTTPRequest $request, ConfirmationTokenChain $confirmationTokenChain)
{
// Safe reload requires manual boot
$this->getApplication()->getKernel()->boot(false);
// Ensure session is started
$request->getSession()->init($request);
// Request with ErrorDirector
$result = ErrorDirector::singleton()->handleRequestWithToken(
$result = ErrorDirector::singleton()->handleRequestWithTokenChain(
$request,
$reloadToken,
$confirmationTokenChain,
$this->getApplication()->getKernel()
);
if ($result) {
@ -97,8 +120,8 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
}
// Fail and redirect the user to the login page
$params = array_merge($request->getVars(), $reloadToken->params(false));
$backURL = $request->getURL() . '?' . http_build_query($params);
$params = array_merge($request->getVars(), $confirmationTokenChain->params(false));
$backURL = $confirmationTokenChain->getRedirectUrlBase() . '?' . http_build_query($params);
$loginPage = Director::absoluteURL(Security::config()->get('login_url'));
$loginPage .= "?BackURL=" . urlencode($backURL);
$result = new HTTPResponse();

View File

@ -21,18 +21,21 @@ class ErrorDirector extends Director
* Redirect with token if allowed, or null if not allowed
*
* @param HTTPRequest $request
* @param ParameterConfirmationToken $token
* @param ConfirmationTokenChain $confirmationTokenChain
* @param Kernel $kernel
* @return null|HTTPResponse Redirection response, or null if not able to redirect
*/
public function handleRequestWithToken(HTTPRequest $request, ParameterConfirmationToken $token, Kernel $kernel)
{
public function handleRequestWithTokenChain(
HTTPRequest $request,
ConfirmationTokenChain $confirmationTokenChain,
Kernel $kernel
) {
Injector::inst()->registerService($request, HTTPRequest::class);
// Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin
$reload = function (HTTPRequest $request) use ($token, $kernel) {
$reload = function (HTTPRequest $request) use ($confirmationTokenChain, $kernel) {
if ($kernel->getEnvironment() === Kernel::DEV || !Security::database_is_ready() || Permission::check('ADMIN')) {
return $token->reloadWithToken();
return $confirmationTokenChain->reloadWithTokens();
}
return null;
};

View File

@ -3,36 +3,28 @@
namespace SilverStripe\Core\Startup;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Security\RandomGenerator;
/**
* Class ParameterConfirmationToken
* This is used to protect dangerous GET parameters that need to be detected early in the request
* lifecycle by generating a one-time-use token & redirecting with that token included in the
* redirected URL
*
* When you need to use a dangerous GET parameter that needs to be set before core/Core.php is
* established, this class takes care of allowing some other code of confirming the parameter,
* by generating a one-time-use token & redirecting with that token included in the redirected URL
*
* WARNING: This class is experimental and designed specifically for use pre-startup.
* It will likely be heavily refactored before the release of 3.2
* @internal This class is designed specifically for use pre-startup and may change without warning
*/
class ParameterConfirmationToken
class ParameterConfirmationToken extends AbstractConfirmationToken
{
/**
* The name of the parameter
*
* @var string
*/
protected $parameterName = null;
/**
* @var HTTPRequest
*/
protected $request = null;
/**
* The parameter given in the main request
*
@ -48,60 +40,6 @@ class ParameterConfirmationToken
protected $parameterBackURL = null;
/**
* The validated and checked token for this parameter
*
* @var string|null A string value, or null if either not provided or invalid
*/
protected $token = null;
protected function pathForToken($token)
{
return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token);
}
/**
* Generate a new random token and store it
*
* @return string Token name
*/
protected function genToken()
{
// Generate a new random token (as random as possible)
$rg = new RandomGenerator();
$token = $rg->randomToken('md5');
// Store a file in the session save path (safer than /tmp, as open_basedir might limit that)
file_put_contents($this->pathForToken($token), $token);
return $token;
}
/**
* Validate a token
*
* @param string $token
* @return boolean True if the token is valid
*/
protected function checkToken($token)
{
if (!$token) {
return false;
}
$file = $this->pathForToken($token);
$content = null;
if (file_exists($file)) {
$content = file_get_contents($file);
unlink($file);
}
return $content == $token;
}
/**
* Create a new ParameterConfirmationToken
*
* @param string $parameterName Name of the querystring parameter to check
* @param HTTPRequest $request
*/
@ -176,54 +114,23 @@ class ParameterConfirmationToken
return $this->parameterBackURL !== null;
}
/**
* Is the necessary token provided for this parameter?
* A value must be provided for the token
*
* @return bool
*/
public function tokenProvided()
{
return !empty($this->token);
}
/**
* Is this parameter requested without a valid token?
*
* @return bool True if the parameter is given without a valid token
*/
public function reloadRequired()
{
return $this->parameterProvided() && !$this->tokenProvided();
}
/**
* Check if this token is provided either in the backurl, or directly,
* but without a token
*
* @return bool
*/
public function reloadRequiredIfError()
{
// Don't reload if token exists
return $this->reloadRequired() || $this->existsInReferer();
}
/**
* Suppress the current parameter by unsetting it from $_GET
*/
public function suppress()
{
unset($_GET[$this->parameterName]);
$this->request->offsetUnset($this->parameterName);
}
/**
* Determine the querystring parameters to include
*
* @param bool $includeToken Include the token value as well?
* @return array List of querystring parameters with name and token parameters
*/
public function params($includeToken = true)
{
$params = array(
@ -235,80 +142,21 @@ class ParameterConfirmationToken
return $params;
}
/**
* Get redirect url, excluding querystring
*
* @return string
*/
protected function currentURL()
public function getRedirectUrlBase()
{
return Controller::join_links(
BASE_URL ?: '/',
$this->request->getURL(false)
);
return ($this->existsInReferer() && !$this->parameterProvided()) ? Director::baseURL() : $this->currentURL();
}
/**
* Get redirection URL
*
* @return string
*/
public function getRedirectUrlParams()
{
return ($this->existsInReferer() && !$this->parameterProvided())
? $this->params()
: array_merge($this->request->getVars(), $this->params());
}
protected function redirectURL()
{
// If url is encoded via BackURL, defer to home page (prevent redirect to form action)
if ($this->existsInReferer() && !$this->parameterProvided()) {
$url = BASE_URL ?: '/';
$params = $this->params();
} else {
$url = $this->currentURL();
$params = array_merge($this->request->getVars(), $this->params());
}
// Merge get params with current url
return Controller::join_links($url, '?' . http_build_query($params));
}
/**
* Forces a reload of the request with the token included
*
* @return HTTPResponse
*/
public function reloadWithToken()
{
$location = $this->redirectURL();
$locationJS = Convert::raw2js($location);
$locationATT = Convert::raw2att($location);
$body = <<<HTML
<script>location.href='$locationJS';</script>
<noscript><meta http-equiv="refresh" content="0; url=$locationATT"></noscript>
You are being redirected. If you are not redirected soon, <a href="$locationATT">click here to continue the flush</a>
HTML;
// Build response
$result = new HTTPResponse($body);
$result->redirect($location);
return $result;
}
/**
* Given a list of token names, suppress all tokens that have not been validated, and
* return the non-validated token with the highest priority
*
* @param array $keys List of token keys in ascending priority (low to high)
* @param HTTPRequest $request
* @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority
*/
public static function prepare_tokens($keys, HTTPRequest $request)
{
$target = null;
foreach ($keys as $key) {
$token = new ParameterConfirmationToken($key, $request);
// Validate this token
if ($token->reloadRequired() || $token->reloadRequiredIfError()) {
$token->suppress();
$target = $token;
}
}
return $target;
$query = http_build_query($this->getRedirectUrlParams());
return Controller::join_links($this->getRedirectUrlBase(), '?' . $query);
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace SilverStripe\Core\Startup;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
/**
* This is used to protect dangerous URLs that need to be detected early in the request lifecycle
* by generating a one-time-use token & redirecting with that token included in the redirected URL
*
* @internal This class is designed specifically for use pre-startup and may change without warning
*/
class URLConfirmationToken extends AbstractConfirmationToken
{
/**
* @var string
*/
protected $urlToCheck;
/**
* @var string
*/
protected $currentURL;
/**
* @var string
*/
protected $tokenParameterName;
/**
* @var bool
*/
protected $urlExistsInBackURL;
/**
* @param string $urlToCheck URL to check
* @param HTTPRequest $request
*/
public function __construct($urlToCheck, HTTPRequest $request)
{
$this->urlToCheck = $urlToCheck;
$this->request = $request;
$this->currentURL = $request->getURL(false);
$this->tokenParameterName = preg_replace('/[^a-z0-9]/i', '', $urlToCheck) . 'token';
$this->urlExistsInBackURL = $this->getURLExistsInBackURL($request);
// If the token provided is valid, mark it as such
$token = $request->getVar($this->tokenParameterName);
if ($this->checkToken($token)) {
$this->token = $token;
}
}
/**
* @param HTTPRequest $request
* @return bool
*/
protected function getURLExistsInBackURL(HTTPRequest $request)
{
$backURL = ltrim($request->getVar('BackURL'), '/');
return (strpos($backURL, $this->urlToCheck) === 0);
}
/**
* @return bool
*/
protected function urlMatches()
{
return ($this->currentURL === $this->urlToCheck);
}
/**
* @return string
*/
public function getURLToCheck()
{
return $this->urlToCheck;
}
/**
* @return bool
*/
public function urlExistsInBackURL()
{
return $this->urlExistsInBackURL;
}
public function reloadRequired()
{
return $this->urlMatches() && !$this->tokenProvided();
}
public function reloadRequiredIfError()
{
return $this->reloadRequired() || $this->urlExistsInBackURL();
}
public function suppress()
{
$_SERVER['REQUEST_URI'] = '/';
$this->request->setURL('/');
}
public function params($includeToken = true)
{
$params = [];
if ($includeToken) {
$params[$this->tokenParameterName] = $this->genToken();
}
return $params;
}
public function currentURL()
{
return Controller::join_links(Director::baseURL(), $this->currentURL);
}
public function getRedirectUrlBase()
{
return ($this->urlExistsInBackURL && !$this->urlMatches()) ? Director::baseURL() : $this->currentURL();
}
public function getRedirectUrlParams()
{
return ($this->urlExistsInBackURL && !$this->urlMatches())
? $this->params()
: array_merge($this->request->getVars(), $this->params());
}
protected function redirectURL()
{
$query = http_build_query($this->getRedirectUrlParams());
return Controller::join_links($this->getRedirectUrlBase(), '?' . $query);
}
}

View File

@ -26,6 +26,7 @@ class Backtrace
array('PDO', '__construct'),
array('mysqli', 'mysqli'),
array('mysqli', 'select_db'),
array('mysqli', 'real_connect'),
array('SilverStripe\\ORM\\DB', 'connect'),
array('SilverStripe\\Security\\Security', 'check_default_admin'),
array('SilverStripe\\Security\\Security', 'encrypt_password'),

View File

@ -54,9 +54,9 @@ class Installer
protected function installHeader()
{
$clientPath = PUBLIC_DIR
? 'resources/vendor/silverstripe/framework/src/Dev/Install/client'
: 'resources/silverstripe/framework/src/Dev/Install/client';
$clientPath = RESOURCES_DIR . (PUBLIC_DIR
? '/vendor/silverstripe/framework/src/Dev/Install/client'
: '/silverstripe/framework/src/Dev/Install/client');
?>
<html>
<head>

View File

@ -106,9 +106,9 @@ if ($installFromCli && ($req->hasErrors() || $dbReq->hasErrors())) {
// Path to client resources (copied through silverstripe/vendor-plugin)
$base = rtrim(BASE_URL, '/') . '/';
$clientPath = PUBLIC_DIR
? 'resources/vendor/silverstripe/framework/src/Dev/Install/client'
: 'resources/silverstripe/framework/src/Dev/Install/client';
$clientPath = RESOURCES_DIR . (PUBLIC_DIR
? '/vendor/silverstripe/framework/src/Dev/Install/client'
: '/silverstripe/framework/src/Dev/Install/client');
// If already installed, ensure the user clicked "reinstall"
$expectedArg = $alreadyInstalled ? 'reinstall' : 'go';

View File

@ -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);
}
}
}
}

View File

@ -17,8 +17,9 @@ class MigrateFileTask extends BuildTask
protected $title = 'Migrate File dataobjects from 3.x';
protected $description
= 'Imports all files referenced by File dataobjects into the new Asset Persistence Layer introduced in 4.0';
protected $description =
'Imports all files referenced by File dataobjects into the new Asset Persistence Layer introduced in 4.0. ' .
'If the task fails or times out, run it again and it will start where it left off.';
public function run($request)
{
@ -27,6 +28,11 @@ class MigrateFileTask extends BuildTask
return;
}
DB::alteration_message(
'If the task fails or times out, run it again and it will start where it left off.',
"info"
);
$migrated = FileMigrationHelper::singleton()->run();
if ($migrated) {
DB::alteration_message("{$migrated} File DataObjects upgraded", "changed");
@ -38,7 +44,6 @@ class MigrateFileTask extends BuildTask
DB::alteration_message("No image thumbnail helper detected", "notice");
return;
}
ImageThumbnailHelper::singleton()->run();
}
}

View File

@ -72,6 +72,7 @@ class CheckboxSetField extends MultiSelectField
{
$selectedValues = $this->getValueArray();
$defaultItems = $this->getDefaultItems();
$disabledItems = $this->getDisabledItems();
// Generate list of options to display
$odd = false;
@ -84,7 +85,7 @@ class CheckboxSetField extends MultiSelectField
$extraClass .= ' val' . preg_replace('/[^a-zA-Z0-9\-\_]/', '_', $itemValue);
$itemChecked = in_array($itemValue, $selectedValues) || in_array($itemValue, $defaultItems);
$itemDisabled = $this->isDisabled() || in_array($itemValue, $defaultItems);
$itemDisabled = $this->isDisabled() || in_array($itemValue, $disabledItems);
$options->push(new ArrayData(array(
'ID' => $itemID,

View File

@ -21,11 +21,13 @@ class CompositeField extends FormField
/**
* Set to true when this field is a readonly field
*
* @var bool
*/
protected $readonly;
/**
* @var $columnCount int Toggle different css-rendering for multiple columns
* @var int Toggle different css-rendering for multiple columns
* ("onecolumn", "twocolumns", "threecolumns"). The content is determined
* by the $children-array, so wrap all items you want to have grouped in a
* column inside a CompositeField.
@ -35,12 +37,12 @@ class CompositeField extends FormField
protected $columnCount = null;
/**
* @var String custom HTML tag to render with, e.g. to produce a <fieldset>.
* @var string custom HTML tag to render with, e.g. to produce a <fieldset>.
*/
protected $tag = 'div';
/**
* @var String Optional description for this set of fields.
* @var string Optional description for this set of fields.
* If the {@link $tag} property is set to use a 'fieldset', this will be
* rendered as a <legend> tag, otherwise its a 'title' attribute.
*/
@ -214,7 +216,7 @@ class CompositeField extends FormField
'tabindex' => null,
'type' => null,
'value' => null,
'title' => ($this->tag == 'fieldset') ? null : $this->legend
'title' => ($this->tag === 'fieldset') ? null : $this->legend
)
);
}

View File

@ -140,12 +140,12 @@ class ConfirmedPasswordField extends FormField
$title = isset($title) ? $title : _t('SilverStripe\\Security\\Member.PASSWORD', 'Password');
// naming with underscores to prevent values from actually being saved somewhere
$this->children = new FieldList(
$this->passwordField = new PasswordField(
$this->children = FieldList::create(
$this->passwordField = PasswordField::create(
"{$name}[_Password]",
$title
),
$this->confirmPasswordfield = new PasswordField(
$this->confirmPasswordfield = PasswordField::create(
"{$name}[_ConfirmPassword]",
(isset($titleConfirmField)) ? $titleConfirmField : _t('SilverStripe\\Security\\Member.CONFIRMPASSWORD', 'Confirm Password')
)
@ -153,11 +153,11 @@ class ConfirmedPasswordField extends FormField
// has to be called in constructor because Field() isn't triggered upon saving the instance
if ($showOnClick) {
$this->children->push($this->hiddenField = new HiddenField("{$name}[_PasswordFieldVisible]"));
$this->getChildren()->push($this->hiddenField = HiddenField::create("{$name}[_PasswordFieldVisible]"));
}
// disable auto complete
foreach ($this->children as $child) {
foreach ($this->getChildren() as $child) {
/** @var FormField $child */
$child->setAttribute('autocomplete', 'off');
}
@ -176,8 +176,8 @@ class ConfirmedPasswordField extends FormField
public function setTitle($title)
{
parent::setTitle($title);
$this->passwordField->setTitle($title);
$this->getPasswordField()->setTitle($title);
return parent::setTitle($title);
}
/**
@ -189,7 +189,7 @@ class ConfirmedPasswordField extends FormField
{
// Build inner content
$fieldContent = '';
foreach ($this->children as $field) {
foreach ($this->getChildren() as $field) {
/** @var FormField $field */
$field->setDisabled($this->isDisabled());
$field->setReadonly($this->isReadonly());
@ -207,8 +207,8 @@ class ConfirmedPasswordField extends FormField
return $fieldContent;
}
if ($this->showOnClickTitle) {
$title = $this->showOnClickTitle;
if ($this->getShowOnClickTitle()) {
$title = $this->getShowOnClickTitle();
} else {
$title = _t(
__CLASS__ . '.SHOWONCLICKTITLE',
@ -286,11 +286,11 @@ class ConfirmedPasswordField extends FormField
/**
* @param string $title
*
* @return ConfirmedPasswordField
* @return $this
*/
public function setRightTitle($title)
{
foreach ($this->children as $field) {
foreach ($this->getChildren() as $field) {
/** @var FormField $field */
$field->setRightTitle($title);
}
@ -310,8 +310,8 @@ class ConfirmedPasswordField extends FormField
public function setChildrenTitles($titles)
{
$expectedChildren = $this->getRequireExistingPassword() ? 3 : 2;
if (is_array($titles) && count($titles) == $expectedChildren) {
foreach ($this->children as $field) {
if (is_array($titles) && count($titles) === $expectedChildren) {
foreach ($this->getChildren() as $field) {
if (isset($titles[0])) {
/** @var FormField $field */
$field->setTitle($titles[0]);
@ -350,7 +350,7 @@ class ConfirmedPasswordField extends FormField
: null;
if ($this->showOnClick && isset($value['_PasswordFieldVisible'])) {
$this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]')
$this->getChildren()->fieldByName($this->getName() . '[_PasswordFieldVisible]')
->setValue($value['_PasswordFieldVisible']);
}
} else {
@ -362,10 +362,10 @@ class ConfirmedPasswordField extends FormField
//looking up field by name is expensive, so lets check it needs to change
if ($oldValue != $this->value) {
$this->children->fieldByName($this->getName() . '[_Password]')
$this->getChildren()->fieldByName($this->getName() . '[_Password]')
->setValue($this->value);
$this->children->fieldByName($this->getName() . '[_ConfirmPassword]')
$this->getChildren()->fieldByName($this->getName() . '[_ConfirmPassword]')
->setValue($this->confirmValue);
}
@ -380,8 +380,8 @@ class ConfirmedPasswordField extends FormField
*/
public function setName($name)
{
$this->passwordField->setName($name . '[_Password]');
$this->confirmPasswordfield->setName($name . '[_ConfirmPassword]');
$this->getPasswordField()->setName($name . '[_Password]');
$this->getConfirmPasswordField()->setName($name . '[_ConfirmPassword]');
if ($this->hiddenField) {
$this->hiddenField->setName($name . '[_PasswordFieldVisible]');
}
@ -417,12 +417,12 @@ class ConfirmedPasswordField extends FormField
return true;
}
$this->passwordField->setValue($this->value);
$this->confirmPasswordfield->setValue($this->confirmValue);
$value = $this->passwordField->Value();
$this->getPasswordField()->setValue($this->value);
$this->getConfirmPasswordField()->setValue($this->confirmValue);
$value = $this->getPasswordField()->Value();
// both password-fields should be the same
if ($value != $this->confirmPasswordfield->Value()) {
if ($value != $this->getConfirmPasswordField()->Value()) {
$validator->validationError(
$name,
_t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSDONTMATCH', "Passwords don't match"),
@ -434,7 +434,7 @@ class ConfirmedPasswordField extends FormField
if (!$this->canBeEmpty) {
// both password-fields shouldn't be empty
if (!$value || !$this->confirmPasswordfield->Value()) {
if (!$value || !$this->getConfirmPasswordField()->Value()) {
$validator->validationError(
$name,
_t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"),
@ -446,29 +446,31 @@ class ConfirmedPasswordField extends FormField
}
// lengths
if (($this->minLength || $this->maxLength)) {
$minLength = $this->getMinLength();
$maxLength = $this->getMaxLength();
if ($minLength || $maxLength) {
$errorMsg = null;
$limit = null;
if ($this->minLength && $this->maxLength) {
$limit = "{{$this->minLength},{$this->maxLength}}";
if ($minLength && $maxLength) {
$limit = "{{$minLength},{$maxLength}}";
$errorMsg = _t(
'SilverStripe\\Forms\\ConfirmedPasswordField.BETWEEN',
__CLASS__ . '.BETWEEN',
'Passwords must be {min} to {max} characters long.',
array('min' => $this->minLength, 'max' => $this->maxLength)
['min' => $minLength, 'max' => $maxLength]
);
} elseif ($this->minLength) {
$limit = "{{$this->minLength}}.*";
} elseif ($minLength) {
$limit = "{{$minLength}}.*";
$errorMsg = _t(
'SilverStripe\\Forms\\ConfirmedPasswordField.ATLEAST',
__CLASS__ . '.ATLEAST',
'Passwords must be at least {min} characters long.',
array('min' => $this->minLength)
['min' => $minLength]
);
} elseif ($this->maxLength) {
$limit = "{0,{$this->maxLength}}";
} elseif ($maxLength) {
$limit = "{0,{$maxLength}}";
$errorMsg = _t(
'SilverStripe\\Forms\\ConfirmedPasswordField.MAXIMUM',
__CLASS__ . '.MAXIMUM',
'Passwords must be at most {max} characters long.',
array('max' => $this->maxLength)
['max' => $maxLength]
);
}
$limitRegex = '/^.' . $limit . '$/';
@ -478,16 +480,18 @@ class ConfirmedPasswordField extends FormField
$errorMsg,
"validation"
);
return false;
}
}
if ($this->requireStrongPassword) {
if ($this->getRequireStrongPassword()) {
if (!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/', $value)) {
$validator->validationError(
$name,
_t(
'SilverStripe\\Forms\\Form.VALIDATIONSTRONGPASSWORD',
"Passwords must have at least one digit and one alphanumeric character"
'Passwords must have at least one digit and one alphanumeric character'
),
"validation"
);
@ -502,8 +506,8 @@ class ConfirmedPasswordField extends FormField
$validator->validationError(
$name,
_t(
'SilverStripe\\Forms\\ConfirmedPasswordField.CURRENT_PASSWORD_MISSING',
"You must enter your current password."
__CLASS__ . '.CURRENT_PASSWORD_MISSING',
'You must enter your current password.'
),
"validation"
);
@ -516,7 +520,7 @@ class ConfirmedPasswordField extends FormField
$validator->validationError(
$name,
_t(
'SilverStripe\\Forms\\ConfirmedPasswordField.LOGGED_IN_ERROR',
__CLASS__ . '.LOGGED_IN_ERROR',
"You must be logged in to change your password."
),
"validation"
@ -532,7 +536,7 @@ class ConfirmedPasswordField extends FormField
$validator->validationError(
$name,
_t(
'SilverStripe\\Forms\\ConfirmedPasswordField.CURRENT_PASSWORD_ERROR',
__CLASS__ . '.CURRENT_PASSWORD_ERROR',
"The current password you have entered is not correct."
),
"validation"
@ -569,7 +573,7 @@ class ConfirmedPasswordField extends FormField
public function performReadonlyTransformation()
{
/** @var ReadonlyField $field */
$field = $this->castedCopy('SilverStripe\\Forms\\ReadonlyField')
$field = $this->castedCopy(ReadonlyField::class)
->setTitle($this->title ? $this->title : _t('SilverStripe\\Security\\Member.PASSWORD', 'Password'))
->setValue('*****');
@ -608,10 +612,84 @@ class ConfirmedPasswordField extends FormField
$currentName = "{$name}[_CurrentPassword]";
if ($show) {
$confirmField = PasswordField::create($currentName, _t('SilverStripe\\Security\\Member.CURRENT_PASSWORD', 'Current Password'));
$this->children->unshift($confirmField);
$this->getChildren()->unshift($confirmField);
} else {
$this->children->removeByName($currentName, true);
$this->getChildren()->removeByName($currentName, true);
}
return $this;
}
/**
* @return PasswordField
*/
public function getPasswordField()
{
return $this->passwordField;
}
/**
* @return PasswordField
*/
public function getConfirmPasswordField()
{
return $this->confirmPasswordfield;
}
/**
* Set the minimum length required for passwords
*
* @param int $minLength
* @return $this
*/
public function setMinLength($minLength)
{
$this->minLength = (int) $minLength;
return $this;
}
/**
* @return int
*/
public function getMinLength()
{
return $this->minLength;
}
/**
* Set the maximum length required for passwords
*
* @param int $maxLength
* @return $this
*/
public function setMaxLength($maxLength)
{
$this->maxLength = (int) $maxLength;
return $this;
}
/**
* @return int
*/
public function getMaxLength()
{
return $this->maxLength;
}
/**
* @param bool $requireStrongPassword
* @return $this
*/
public function setRequireStrongPassword($requireStrongPassword)
{
$this->requireStrongPassword = (bool) $requireStrongPassword;
return $this;
}
/**
* @return bool
*/
public function getRequireStrongPassword()
{
return $this->requireStrongPassword;
}
}

View File

@ -39,9 +39,8 @@ class CurrencyField extends TextField
{
if ($this->value) {
return preg_replace('/[^0-9.\-]/', '', $this->value);
} else {
return 0.00;
}
return 0.00;
}
public function Type()
@ -54,7 +53,7 @@ class CurrencyField extends TextField
*/
public function performReadonlyTransformation()
{
return $this->castedCopy('SilverStripe\\Forms\\CurrencyField_Readonly');
return $this->castedCopy(CurrencyField_Readonly::class);
}
public function validate($validator)

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Forms;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\FieldType\DBCurrency;
/**
* Readonly version of a {@link CurrencyField}.
@ -13,7 +14,7 @@ class CurrencyField_Disabled extends CurrencyField
protected $disabled = true;
/**
* overloaded to display the correctly formated value for this datatype
* Overloaded to display the correctly formatted value for this data type
*
* @param array $properties
* @return string
@ -22,7 +23,8 @@ class CurrencyField_Disabled extends CurrencyField
{
if ($this->value) {
$val = Convert::raw2xml($this->value);
$val = _t('SilverStripe\\Forms\\CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.-]/', "", $val), 2);
$val = DBCurrency::config()->get('currency_symbol')
. number_format(preg_replace('/[^0-9.-]/', '', $val), 2);
$valforInput = Convert::raw2att($val);
} else {
$valforInput = '';

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Forms;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\FieldType\DBCurrency;
/**
* Readonly version of a {@link CurrencyField}.
@ -11,19 +12,20 @@ class CurrencyField_Readonly extends ReadonlyField
{
/**
* Overloaded to display the correctly formated value for this datatype
* Overloaded to display the correctly formatted value for this data type
*
* @param array $properties
* @return string
*/
public function Field($properties = array())
{
$currencySymbol = DBCurrency::config()->get('currency_symbol');
if ($this->value) {
$val = Convert::raw2xml($this->value);
$val = _t('SilverStripe\\Forms\\CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.-]/', "", $val), 2);
$val = $currencySymbol . number_format(preg_replace('/[^0-9.-]/', '', $val), 2);
$valforInput = Convert::raw2att($val);
} else {
$val = '<i>' . _t('SilverStripe\\Forms\\CurrencyField.CURRENCYSYMBOL', '$') . '0.00</i>';
$val = '<i>' . $currencySymbol . '0.00</i>';
$valforInput = '';
}
return "<span class=\"readonly " . $this->extraClass() . "\" id=\"" . $this->ID() . "\">$val</span>"

View File

@ -10,7 +10,7 @@ use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\ValidationResult;
/**
* Form used for editing a date stirng
* Form used for editing a date string
*
* Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
* since the required frontend dependencies are included through CMS bundling.

View File

@ -30,6 +30,7 @@ class DefaultFormFactory implements FormFactory
* @param string $name
* @param array $context
* @return Form
* @throws InvalidArgumentException When required context is missing
*/
public function getForm(RequestHandler $controller = null, $name = FormFactory::DEFAULT_NAME, $context = [])
{

View File

@ -106,7 +106,7 @@ class DropdownField extends SingleSelectField
}
return new ArrayData(array(
'Title' => $title,
'Title' => (string)$title,
'Value' => $value,
'Selected' => $selected,
'Disabled' => $disabled,

View File

@ -286,6 +286,9 @@ class Form extends ViewableData implements HasRequestHandler
) {
parent::__construct();
$fields = $fields ? $fields : FieldList::create();
$actions = $actions ? $actions : FieldList::create();
$fields->setForm($this);
$actions->setForm($this);
@ -484,11 +487,13 @@ class Form extends ViewableData implements HasRequestHandler
// Set message on either a field or the parent form
foreach ($result->getMessages() as $message) {
$fieldName = $message['fieldName'];
if ($fieldName) {
$owner = $this->fields->dataFieldByName($fieldName) ?: $this;
} else {
$owner = $this;
}
$owner->setMessage($message['message'], $message['messageType'], $message['messageCast']);
}
return $this;
@ -772,7 +777,9 @@ class Form extends ViewableData implements HasRequestHandler
*/
public function setFields($fields)
{
$fields->setForm($this);
$this->fields = $fields;
return $this;
}
@ -794,7 +801,9 @@ class Form extends ViewableData implements HasRequestHandler
*/
public function setActions($actions)
{
$actions->setForm($this);
$this->actions = $actions;
return $this;
}
@ -1405,8 +1414,9 @@ class Form extends ViewableData implements HasRequestHandler
$submitted = true;
}
// dont include fields without data
// Don't include fields without data
$dataFields = $this->Fields()->dataFields();
if (!$dataFields) {
return $this;
}

View File

@ -295,8 +295,8 @@ class FormField extends RequestHandler
*
* Examples:
*
* - 'TotalAmount' will return 'Total Amount'
* - 'Organisation.ZipCode' will return 'Organisation Zip Code'
* - 'TotalAmount' will return 'Total amount'
* - 'Organisation.ZipCode' will return 'Organisation zip code'
*
* @param string $fieldName
*
@ -304,15 +304,27 @@ class FormField extends RequestHandler
*/
public static function name_to_label($fieldName)
{
// Handle dot delimiters
if (strpos($fieldName, '.') !== false) {
$parts = explode('.', $fieldName);
$label = $parts[count($parts) - 2] . ' ' . $parts[count($parts) - 1];
// Ensure that any letter following a dot is uppercased, so that the regex below can break it up
// into words
$label = implode(array_map('ucfirst', $parts));
} else {
$label = $fieldName;
}
return preg_replace('/([a-z]+)([A-Z])/', '$1 $2', $label);
// Replace any capital letter that is followed by a lowercase letter with a space, the lowercased
// version of itself then the remaining lowercase letters.
$labelWithSpaces = preg_replace_callback('/([A-Z])([a-z]+)/', function ($matches) {
return ' ' . strtolower($matches[1]) . $matches[2];
}, $label);
// Add a space before any capital letter block that is at the end of the string
$labelWithSpaces = preg_replace('/([a-z])([A-Z]+)$/', '$1 $2', $labelWithSpaces);
// The first letter should be uppercase
return ucfirst(trim($labelWithSpaces));
}
/**
@ -1424,7 +1436,7 @@ class FormField extends RequestHandler
$field = $classOrCopy;
if (!is_object($field)) {
$field = new $classOrCopy($this->name);
$field = $classOrCopy::create($this->name);
}
$field

View File

@ -0,0 +1,17 @@
<?php
namespace SilverStripe\Forms\GridField\FormAction;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
abstract class AbstractRequestAwareStore implements StateStore
{
/**
* @return HTTPRequest
*/
public function getRequest()
{
// Replicating existing functionality from GridField_FormAction
return Controller::curr()->getRequest();
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\Forms\GridField\FormAction;
/**
* Stores GridField action state on an attribute on the action and then analyses request parameters to load it back
*/
class AttributeStore extends AbstractRequestAwareStore
{
/**
* Save the given state against the given ID returning an associative array to be added as attributes on the form
* action
*
* @param string $id
* @param array $state
* @return array
*/
public function save($id, array $state)
{
// Just save the state in the attributes of the action
return [
'data-action-state' => json_encode($state),
];
}
/**
* Load state for a given ID
*
* @param string $id
* @return array
*/
public function load($id)
{
// Check the request
return (array) json_decode((string) $this->getRequest()->requestVar('ActionState'), true);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace SilverStripe\Forms\GridField\FormAction;
use SilverStripe\Control\HTTPRequest;
/**
* Stores GridField action state in the session in exactly the same way it has in the past
*/
class SessionStore extends AbstractRequestAwareStore implements StateStore
{
/**
* Save the given state against the given ID returning an associative array to be added as attributes on the form
* action
*
* @param string $id
* @param array $state
* @return array
*/
public function save($id, array $state)
{
$this->getRequest()->getSession()->set($id, $state);
// This adapter does not require any additional attributes...
return [];
}
/**
* Load state for a given ID
*
* @param string $id
* @return array
*/
public function load($id)
{
return (array) $this->getRequest()->getSession()->get($id);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\Forms\GridField\FormAction;
interface StateStore
{
/**
* Save the given state against the given ID returning an associative array to be added as attributes on the form
* action
*
* @param string $id
* @param array $state
* @return array
*/
public function save($id, array $state);
/**
* Load state for a given ID
*
* @param string $id
* @return array
*/
public function load($id);
}

View File

@ -9,8 +9,11 @@ use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\GridField\FormAction\SessionStore;
use SilverStripe\Forms\GridField\FormAction\StateStore;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
@ -113,11 +116,13 @@ class GridField extends FormField
protected $readonlyComponents = [
GridField_ActionMenu::class,
GridFieldConfig_RecordViewer::class,
GridFieldButtonRow::class,
GridFieldDataColumns::class,
GridFieldDetailForm::class,
GridFieldLazyLoader::class,
GridFieldPageCount::class,
GridFieldPaginator::class,
GridFieldFilterHeader::class,
GridFieldSortableHeader::class,
GridFieldToolbarHeader::class,
GridFieldViewButton::class,
@ -241,16 +246,22 @@ class GridField extends FormField
{
$copy = clone $this;
$copy->setReadonly(true);
$copyConfig = $copy->getConfig();
// get the whitelist for allowable readonly components
$allowedComponents = $this->getReadonlyComponents();
foreach ($this->getConfig()->getComponents() as $component) {
// if a component doesn't exist, remove it from the readonly version.
if (!in_array(get_class($component), $allowedComponents)) {
$copy->getConfig()->removeComponent($component);
$copyConfig->removeComponent($component);
}
}
// As the edit button may have been removed, add a view button if it doesn't have one
if (!$copyConfig->getComponentByType(GridFieldViewButton::class)) {
$copyConfig->addComponent(new GridFieldViewButton);
}
return $copy;
}
@ -290,6 +301,18 @@ class GridField extends FormField
return $this;
}
/**
* @param bool $readonly
*
* @return $this
*/
public function setReadonly($readonly)
{
parent::setReadonly($readonly);
$this->getState()->Readonly = $readonly;
return $this;
}
/**
* @return ArrayList
*/
@ -989,9 +1012,14 @@ class GridField extends FormField
$state->setValue($fieldData['GridState']);
}
// Fetch the store for the "state" of actions (not the GridField)
/** @var StateStore $store */
$store = Injector::inst()->create(StateStore::class . '.' . $this->getName());
foreach ($data as $dataKey => $dataValue) {
if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
$stateChange = $request->getSession()->get($matches[1]);
$stateChange = $store->load($matches[1]);
$actionName = $stateChange['actionName'];
$arguments = array();
@ -1009,6 +1037,9 @@ class GridField extends FormField
}
if ($request->getHeader('X-Pjax') === 'CurrentField') {
if ($this->getState()->Readonly === true) {
$this->performDisabledTransformation();
}
return $this->FieldHolder();
}

View File

@ -10,8 +10,10 @@ class GridFieldConfig_RecordEditor extends GridFieldConfig
/**
*
* @param int $itemsPerPage - How many items per page should show up
* @param bool $showPagination Whether the `Previous` and `Next` buttons should display or not, leave as null to use default
* @param bool $showAdd Whether the `Add` button should display or not, leave as null to use default
*/
public function __construct($itemsPerPage = null)
public function __construct($itemsPerPage = null, $showPagination = null, $showAdd = null)
{
parent::__construct();
@ -26,7 +28,7 @@ class GridFieldConfig_RecordEditor extends GridFieldConfig
$this->addComponent(new GridField_ActionMenu());
$this->addComponent(new GridFieldPageCount('toolbar-header-right'));
$this->addComponent($pagination = new GridFieldPaginator($itemsPerPage));
$this->addComponent(new GridFieldDetailForm());
$this->addComponent(new GridFieldDetailForm(null, $showPagination, $showAdd));
$sort->setThrowExceptionOnBadDataType(false);
$filter->setThrowExceptionOnBadDataType(false);

View File

@ -38,11 +38,20 @@ class GridFieldDetailForm implements GridField_URLHandler
protected $template = null;
/**
*
* @var string
*/
protected $name;
/**
* @var bool
*/
protected $showPagination;
/**
* @var bool
*/
protected $showAdd;
/**
* @var Validator The form validator used for both add and edit fields.
*/
@ -79,10 +88,14 @@ class GridFieldDetailForm implements GridField_URLHandler
* controller who wants to display the getCMSFields
*
* @param string $name The name of the edit form to place into the pop-up form
* @param bool $showPagination Whether the `Previous` and `Next` buttons should display or not, leave as null to use default
* @param bool $showAdd Whether the `Add` button should display or not, leave as null to use default
*/
public function __construct($name = 'DetailForm')
public function __construct($name = null, $showPagination = null, $showAdd = null)
{
$this->name = $name;
$this->setName($name ?: 'DetailForm');
$this->setShowPagination($showPagination);
$this->setShowAdd($showAdd);
}
/**
@ -93,6 +106,10 @@ class GridFieldDetailForm implements GridField_URLHandler
*/
public function handleItem($gridField, $request)
{
if ($gridStateStr = $request->getVar('gridState')) {
$gridField->getState(false)->setValue($gridStateStr);
}
// Our getController could either give us a true Controller, if this is the top-level GridField.
// It could also give us a RequestHandler in the form of GridFieldDetailForm_ItemRequest if this is a
// nested GridField.
@ -102,7 +119,7 @@ class GridFieldDetailForm implements GridField_URLHandler
if (is_numeric($request->param('ID'))) {
/** @var Filterable $dataList */
$dataList = $gridField->getList();
$record = $dataList->byID($request->param("ID"));
$record = $dataList->byID($request->param('ID'));
} else {
$record = Injector::inst()->create($gridField->getModelClass());
}
@ -179,6 +196,68 @@ class GridFieldDetailForm implements GridField_URLHandler
return $this->name;
}
/**
* @return bool
*/
protected function getDefaultShowPagination()
{
$formActionsConfig = GridFieldDetailForm_ItemRequest::config()->get('formActions');
return isset($formActionsConfig['showPagination']) ? (bool) $formActionsConfig['showPagination'] : false;
}
/**
* @return bool
*/
public function getShowPagination()
{
if ($this->showPagination === null) {
return $this->getDefaultShowPagination();
}
return (bool) $this->showPagination;
}
/**
* @param bool|null $showPagination
* @return GridFieldDetailForm
*/
public function setShowPagination($showPagination)
{
$this->showPagination = $showPagination;
return $this;
}
/**
* @return bool
*/
protected function getDefaultShowAdd()
{
$formActionsConfig = GridFieldDetailForm_ItemRequest::config()->get('formActions');
return isset($formActionsConfig['showAdd']) ? (bool) $formActionsConfig['showAdd'] : false;
}
/**
* @return bool
*/
public function getShowAdd()
{
if ($this->showAdd === null) {
return $this->getDefaultShowAdd();
}
return (bool) $this->showAdd;
}
/**
* @param bool|null $showAdd
* @return GridFieldDetailForm
*/
public function setShowAdd($showAdd)
{
$this->showAdd = $showAdd;
return $this;
}
/**
* @param Validator $validator
* @return $this
@ -232,11 +311,10 @@ class GridFieldDetailForm implements GridField_URLHandler
{
if ($this->itemRequestClass) {
return $this->itemRequestClass;
} elseif (ClassInfo::exists(static::class . "_ItemRequest")) {
return static::class . "_ItemRequest";
} else {
return GridFieldDetailForm_ItemRequest::class;
} elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
return static::class . '_ItemRequest';
}
return GridFieldDetailForm_ItemRequest::class;
}
/**

View File

@ -7,9 +7,12 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
@ -268,6 +271,48 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
return $form;
}
/**
* @return CompositeField Returns the right aligned toolbar group field along with its FormAction's
*/
protected function getRightGroupField()
{
$rightGroup = CompositeField::create()->setName('RightGroup');
$rightGroup->addExtraClass('ml-auto');
$rightGroup->setFieldHolderTemplate(get_class($rightGroup) . '_holder_buttongroup');
$previousAndNextGroup = CompositeField::create()->setName('PreviousAndNextGroup');
$previousAndNextGroup->addExtraClass('circular-group mr-2');
$previousAndNextGroup->setFieldHolderTemplate(get_class($previousAndNextGroup) . '_holder_buttongroup');
/** @var GridFieldDetailForm $component */
$component = $this->gridField->getConfig()->getComponentByType(GridFieldDetailForm::class);
$gridState = $this->getRequest()->requestVar('gridState');
if ($component && $component->getShowPagination()) {
$previousAndNextGroup->push(FormAction::create('doPrevious')
->setUseButtonTag(true)
->setAttribute('data-grid-state', $gridState)
->setDisabled(!$this->getPreviousRecordID())
->addExtraClass('btn btn-secondary font-icon-left-open action--previous discard-confirmation'));
$previousAndNextGroup->push(FormAction::create('doNext')
->setUseButtonTag(true)
->setAttribute('data-grid-state', $gridState)
->setDisabled(!$this->getNextRecordID())
->addExtraClass('btn btn-secondary font-icon-right-open action--next discard-confirmation'));
}
$rightGroup->push($previousAndNextGroup);
if ($component && $component->getShowAdd()) {
$rightGroup->push(FormAction::create('doNew')
->setUseButtonTag(true)
->setAttribute('data-grid-state', $this->getRequest()->getVar('gridState'))
->addExtraClass('btn btn-primary font-icon-plus-thin circular action--new discard-confirmation'));
}
return $rightGroup;
}
/**
* Build the set of form field actions for this DataObject
*
@ -275,26 +320,31 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
*/
protected function getFormActions()
{
$canEdit = $this->record->canEdit();
$canDelete = $this->record->canDelete();
$actions = new FieldList();
if ($this->record->ID !== 0) {
if ($canEdit) {
if ($this->record->ID !== 0) { // existing record
if ($this->record->canEdit()) {
$actions->push(FormAction::create('doSave', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Save', 'Save'))
->setUseButtonTag(true)
->addExtraClass('btn-primary font-icon-save'));
}
if ($canDelete) {
if ($this->record->canDelete()) {
$actions->push(FormAction::create('doDelete', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Delete', 'Delete'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-danger btn-hide-outline font-icon-trash-bin action--delete'));
}
$gridState = $this->getRequest()->requestVar('gridState');
$this->gridField->getState(false)->setValue($gridState);
$actions->push(HiddenField::create('gridState', null, $gridState));
$actions->push($this->getRightGroupField());
} else { // adding new record
//Change the Save label to 'Create'
$actions->push(FormAction::create('doSave', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Create', 'Create'))
->setUseButtonTag(true)
->addExtraClass('btn-primary font-icon-plus'));
->addExtraClass('btn-primary font-icon-plus-thin'));
// Add a Cancel link which is a button-like link and link back to one level up.
$crumbs = $this->Breadcrumbs();
@ -309,7 +359,9 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
$actions->push(new LiteralField('cancelbutton', $text));
}
}
$this->extend('updateFormActions', $actions);
return $actions;
}
@ -410,6 +462,108 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
return $this->redirectAfterSave($isNewRecord);
}
/**
* Goes to the previous record
* @param array $data The form data
* @param Form $form The Form object
* @return HTTPResponse
*/
public function doPrevious($data, $form)
{
$this->getToplevelController()->getResponse()->addHeader('X-Pjax', 'Content');
$link = $this->getEditLink($this->getPreviousRecordID());
return $this->redirect($link);
}
/**
* Goes to the next record
* @param array $data The form data
* @param Form $form The Form object
* @return HTTPResponse
*/
public function doNext($data, $form)
{
$this->getToplevelController()->getResponse()->addHeader('X-Pjax', 'Content');
$link = $this->getEditLink($this->getNextRecordID());
return $this->redirect($link);
}
/**
* Creates a new record. If you're already creating a new record,
* this forces the URL to change.
*
* @param array $data The form data
* @param Form $form The Form object
* @return HTTPResponse
*/
public function doNew($data, $form)
{
return $this->redirect(Controller::join_links($this->gridField->Link('item'), 'new'));
}
/**
* Gets the edit link for a record
*
* @param int $id The ID of the record in the GridField
* @return string
*/
public function getEditLink($id)
{
return Controller::join_links(
$this->gridField->Link(),
'item',
$id,
'?gridState=' . urlencode($this->gridField->getState(false)->Value())
);
}
/**
* @param int $offset The offset from the current record
* @return int|bool
*/
private function getAdjacentRecordID($offset)
{
$gridField = $this->getGridField();
$gridStateStr = $this->getRequest()->requestVar('gridState');
$state = $gridField->getState(false);
$state->setValue($gridStateStr);
$data = $state->getData();
$paginator = $data->getData('GridFieldPaginator');
if (!$paginator) {
return false;
}
$currentPage = $paginator->getData('currentPage');
$itemsPerPage = $paginator->getData('itemsPerPage');
$limit = $itemsPerPage + 2;
$limitOffset = max(0, $itemsPerPage * ($currentPage-1) -1);
$map = $gridField->getManipulatedList()->limit($limit, $limitOffset)->column('ID');
$index = array_search($this->record->ID, $map);
return isset($map[$index+$offset]) ? $map[$index+$offset] : false;
}
/**
* Gets the ID of the previous record in the list.
*
* @return int
*/
public function getPreviousRecordID()
{
return $this->getAdjacentRecordID(-1);
}
/**
* Gets the ID of the next record in the list.
*
* @return int
*/
public function getNextRecordID()
{
return $this->getAdjacentRecordID(1);
}
/**
* Response object for this request after a successful save
*

View File

@ -62,7 +62,12 @@ class GridFieldEditButton implements GridField_ColumnProvider, GridField_ActionP
*/
public function getUrl($gridField, $record, $columnName)
{
return Controller::join_links($gridField->Link('item'), $record->ID, 'edit');
return Controller::join_links(
$gridField->Link('item'),
$record->ID,
'edit',
'?gridState=' . urlencode($gridField->getState(false)->Value())
);
}
/**

View File

@ -6,6 +6,7 @@ use LogicException;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\FieldGroup;
@ -43,6 +44,16 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
*/
public $useLegacyFilterHeader = false;
/**
* Forces all filter components to revert to displaying the legacy
* table header style rather than the react driven search box
*
* @deprecated 4.3.0:5.0.0 Will be removed in 5.0
* @config
* @var bool
*/
private static $force_legacy = false;
/**
* @var \SilverStripe\ORM\Search\SearchContext
*/
@ -76,7 +87,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
}
/**
* @param bool $useLegacy
* @param bool $useLegacy This will be removed in 5.0
* @param callable|null $updateSearchContext This will be removed in 5.0
* @param callable|null $updateSearchForm This will be removed in 5.0
*/
@ -85,7 +96,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
callable $updateSearchContext = null,
callable $updateSearchForm = null
) {
$this->useLegacyFilterHeader = $useLegacy;
$this->useLegacyFilterHeader = Config::inst()->get(self::class, 'force_legacy') || $useLegacy;
$this->updateSearchContextCallback = $updateSearchContext;
$this->updateSearchFormCallback = $updateSearchForm;
}
@ -154,7 +165,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
* If the GridField has a filterable datalist, return an array of actions
*
* @param GridField $gridField
* @return array
* @return void
*/
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
{
@ -163,14 +174,13 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
}
$state = $gridField->State->GridFieldFilterHeader;
$state->Columns = null;
if ($actionName === 'filter') {
if (isset($data['filter'][$gridField->getName()])) {
foreach ($data['filter'][$gridField->getName()] as $key => $filter) {
$state->Columns->$key = $filter;
}
}
} elseif ($actionName === 'reset') {
$state->Columns = null;
}
}
@ -193,12 +203,10 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
$filterArguments = $columns->toArray();
$dataListClone = clone($dataList);
foreach ($filterArguments as $columnName => $value) {
if ($dataList->canFilterBy($columnName) && $value) {
$dataListClone = $dataListClone->filter($columnName . ':PartialMatch', $value);
}
}
return $dataListClone;
$results = $this->getSearchContext($gridField)
->getQuery($filterArguments, false, false, $dataListClone);
return $results;
}
/**
@ -281,14 +289,18 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
}, array_keys($filters)), $filters);
}
$searchAction = GridField_FormAction::create($gridField, 'filter', false, 'filter', null);
$clearAction = GridField_FormAction::create($gridField, 'reset', false, 'reset', null);
$schema = [
'formSchemaUrl' => $schemaUrl,
'name' => $searchField,
'placeholder' => _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $name]),
'filters' => $filters ?: new \stdClass, // stdClass maps to empty json object '{}'
'gridfield' => $gridField->getName(),
'searchAction' => GridField_FormAction::create($gridField, 'filter', false, 'filter', null)->getAttribute('name'),
'clearAction' => GridField_FormAction::create($gridField, 'reset', false, 'reset', null)->getAttribute('name')
'searchAction' => $searchAction->getAttribute('name'),
'searchActionState' => $searchAction->getAttribute('data-action-state'),
'clearAction' => $clearAction->getAttribute('name'),
'clearActionState' => $clearAction->getAttribute('data-action-state'),
];
return json_encode($schema);
@ -337,9 +349,11 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi
$field->addExtraClass('stacked');
}
$name = $gridField->Title ?: singleton($gridField->getModelClass())->i18n_plural_name();
$this->searchForm = $form = new Form(
$gridField,
"SearchForm",
$name . "SearchForm",
$searchFields,
new FieldList()
);

View File

@ -144,6 +144,7 @@ class GridFieldPaginator implements GridField_HTMLProvider, GridField_DataManipu
// Force the state to the initial page if none is set
$state->currentPage(1);
$state->itemsPerPage($this->getItemsPerPage());
return $state;
}

View File

@ -3,8 +3,10 @@
namespace SilverStripe\Forms\GridField;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\GridField\FormAction\StateStore;
/**
* This class is the base class when you want to have an action that alters the state of the
@ -12,6 +14,11 @@ use SilverStripe\Forms\FormAction;
*/
class GridField_FormAction extends FormAction
{
/**
* A common string prefix for keys generated to store form action "state" against
*/
const STATE_KEY_PREFIX = 'gf_';
/**
* @var GridField
*/
@ -80,28 +87,35 @@ class GridField_FormAction extends FormAction
*/
public function getAttributes()
{
// Store state in session, and pass ID to client side.
// Determine the state that goes with this action
$state = array(
'grid' => $this->getNameFromParent(),
'actionName' => $this->actionName,
'args' => $this->args,
);
// Ensure $id doesn't contain only numeric characters
$id = 'gf_' . substr(md5(serialize($state)), 0, 8);
// Generate a key and attach it to the action name
$key = static::STATE_KEY_PREFIX . substr(md5(serialize($state)), 0, 8);
// Note: This field needs to be less than 65 chars, otherwise Suhosin security patch will strip it
$name = 'action_gridFieldAlterAction?StateID=' . $key;
$session = Controller::curr()->getRequest()->getSession();
$session->set($id, $state);
$actionData['StateID'] = $id;
// Define attributes
$attributes = array(
'name' => $name,
'data-url' => $this->gridField->Link(),
'type' => "button",
);
// Create a "store" for the "state" of this action
/** @var StateStore $store */
$store = Injector::inst()->create(StateStore::class . '.' . $this->gridField->getName());
// Store the state and update attributes as required
$attributes += $store->save($key, $state);
// Return attributes
return array_merge(
parent::getAttributes(),
array(
// Note: This field needs to be less than 65 chars, otherwise Suhosin security patch
// will strip it from the requests
'name' => 'action_gridFieldAlterAction' . '?' . http_build_query($actionData),
'data-url' => $this->gridField->Link(),
)
$attributes
);
}

View File

@ -83,6 +83,7 @@ abstract class HTMLEditorConfig
// Create new instance if unconfigured
if (!isset(self::$configs[$identifier])) {
self::$configs[$identifier] = static::create();
self::$configs[$identifier]->setOption('editorIdentifier', $identifier);
}
return self::$configs[$identifier];
}
@ -98,6 +99,7 @@ abstract class HTMLEditorConfig
{
if ($config) {
self::$configs[$identifier] = $config;
self::$configs[$identifier]->setOption('editorIdentifier', $identifier);
} else {
unset(self::$configs[$identifier]);
}

View File

@ -130,11 +130,13 @@ class TinyMCECombinedGenerator implements TinyMCEScriptGenerator, Flushable
// Register vars for config
$baseDirJS = Convert::raw2js(Director::absoluteBaseURL());
$name = Convert::raw2js($this->checkName($config));
$buffer = [];
$buffer[] = <<<SCRIPT
(function() {
var baseTag = window.document.getElementsByTagName('base');
var baseURL = baseTag.length ? baseTag[0].baseURI : '$baseDirJS';
var editorIdentifier = '$name';
SCRIPT;
$buffer[] = <<<SCRIPT
(function() {

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Forms;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBMultiEnum;
use SilverStripe\ORM\Relation;
/**
@ -98,7 +99,15 @@ abstract class MultiSelectField extends SelectField
$value = array_values($relation->getIDList());
parent::setValue($value);
} elseif ($record->hasField($fieldName)) {
$value = $this->stringDecode($record->$fieldName);
// Load dataValue from field... a CSV for DBMultiEnum
if ($record->obj($fieldName) instanceof DBMultiEnum) {
$value = $this->csvDecode($record->$fieldName);
// ... JSON-encoded string for other fields
} else {
$value = $this->stringDecode($record->$fieldName);
}
parent::setValue($value);
}
}
@ -129,8 +138,14 @@ abstract class MultiSelectField extends SelectField
// Save ids into relation
$relation->setByIDList($items);
} elseif ($record->hasField($fieldName)) {
// Save dataValue into field
$record->$fieldName = $this->stringEncode($items);
// Save dataValue into field... a CSV for DBMultiEnum
if ($record->obj($fieldName) instanceof DBMultiEnum) {
$record->$fieldName = $this->csvEncode($items);
// ... JSON-encoded string for other fields
} else {
$record->$fieldName = $this->stringEncode($items);
}
}
}
@ -169,6 +184,45 @@ abstract class MultiSelectField extends SelectField
throw new \InvalidArgumentException("Invalid string encoded value for multi select field");
}
/**
* Encode a list of values into a string as a comma separated list.
* Commas will be stripped from the items passed in
*
* @param array $value
* @return string|null
*/
protected function csvEncode($value)
{
if (!$value) {
return null;
}
return implode(
',',
array_map(
function ($x) {
return str_replace(',', '', $x);
},
array_values($value)
)
);
}
/**
* Decode a list of values from a comma separated string.
* Spaces are trimmed
*
* @param string $value
* @return array
*/
protected function csvDecode($value)
{
if (!$value) {
return [];
}
return preg_split('/\s*,\s*/', trim($value));
}
/**
* Validate this field
*

View File

@ -19,9 +19,9 @@ class PrintableTransformation_TabSet extends TabSet
public function FieldHolder($properties = array())
{
// This gives us support for sub-tabs.
$tag = ($this->tabSet) ? "h2>" : "h1>";
$tag = $this->getTabSet() ? 'h2>' : 'h1>';
$retVal = '';
foreach ($this->children as $tab) {
foreach ($this->getChildren() as $tab) {
$retVal .= "<$tag" . $tab->Title() . "</$tag\n";
$retVal .= $tab->FieldHolder();
}

View File

@ -94,7 +94,7 @@ class SingleLookupField extends SingleSelectField
return $label;
}
return $value;
return parent::Value();
}
/**

View File

@ -106,9 +106,8 @@ class TabSet extends CompositeField
{
if ($this->tabSet) {
return $this->tabSet->ID() . '_' . $this->id . '_set';
} else {
return $this->id;
}
return $this->id;
}
/**

View File

@ -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))) {

View File

@ -17,7 +17,7 @@ class TreeDropdownField_Readonly extends TreeDropdownField
}
$source = [ $this->value => $title ];
$field = new LookupField($this->name, $this->title, $source);
$field = LookupField::create($this->name, $this->title, $source);
$field->setValue($this->value);
$field->setForm($this->form);
return $field->Field();

View File

@ -267,4 +267,34 @@ class TreeMultiselectField extends TreeDropdownField
$copy->setTitleField($this->getTitleField());
return $copy;
}
/**
* {@inheritdoc}
*
* @internal To be removed in 5.0
*/
protected function objectForKey($key)
{
/**
* Fixes https://github.com/silverstripe/silverstripe-framework/issues/8332
*
* Due to historic reasons, the default (empty) value for this field is 'unchanged', even though
* the field is usually integer on the database side.
* MySQL handles that gracefully and returns an empty result in that case,
* whereas some other databases (e.g. PostgreSQL) do not support comparison
* of numeric types with string values, issuing a database error.
*
* This fix is not ideal, but supposed to keep backward compatibility for SS4.
*
* In 5.0 this method to be removed and NULL should be used instead of 'unchanged' (or an empty array. to be decided).
* In 5.0 this class to be refactored so that $this->value is always an array of values (or null)
*/
if ($this->getKeyField() === 'ID' && $key === 'unchanged') {
$key = null;
} elseif (is_string($key)) {
$key = preg_split('/\s*,\s*/', trim($key));
}
return parent::objectForKey($key);
}
}

View File

@ -97,7 +97,7 @@ class DebugViewFriendlyErrorFormatter implements FormatterInterface
public function format(array $record)
{
// Get error code
$code = empty($record['code']) ? $this->statusCode : $record['code'];
$code = empty($record['code']) ? $this->getStatusCode() : $record['code'];
return $this->output($code);
}
@ -127,8 +127,9 @@ class DebugViewFriendlyErrorFormatter implements FormatterInterface
$output = $renderer->renderHeader();
$output .= $renderer->renderInfo("Website Error", $this->getTitle(), $this->getBody());
if (Email::config()->admin_email) {
$mailto = Email::obfuscate(Email::config()->admin_email);
$adminEmail = Email::config()->get('admin_email');
if ($adminEmail) {
$mailto = Email::obfuscate($adminEmail);
$output .= $renderer->renderParagraph('Contact an administrator: ' . $mailto . '');
}

View File

@ -25,7 +25,7 @@ class DetailedErrorFormatter implements FormatterInterface
);
} else {
$context = isset($record['context']) ? $record['context'] : $record;
foreach (array('code','message','file','line') as $key) {
foreach (['code', 'message', 'file', 'line'] as $key) {
if (!isset($context[$key])) {
$context[$key] = isset($record[$key]) ? $record[$key] : null;
}
@ -57,7 +57,7 @@ class DetailedErrorFormatter implements FormatterInterface
public function formatBatch(array $records)
{
return implode("\n", array_map(array($this, 'format'), $records));
return implode("\n", array_map([$this, 'format'], $records));
}
/**

View File

@ -18,7 +18,7 @@ class HTTPOutputHandler extends AbstractProcessingHandler
/**
* @var string
*/
private $contentType = "text/html";
private $contentType = 'text/html';
/**
* @var int
@ -155,8 +155,8 @@ class HTTPOutputHandler extends AbstractProcessingHandler
// If headers have been sent then these won't be used, and may throw errors that we wont' want to see.
if (!headers_sent()) {
$response->setStatusCode($this->statusCode);
$response->addHeader("Content-Type", $this->contentType);
$response->setStatusCode($this->getStatusCode());
$response->addHeader('Content-Type', $this->getContentType());
} else {
// To supress errors aboot errors
$response->setStatusCode(200);
@ -165,6 +165,6 @@ class HTTPOutputHandler extends AbstractProcessingHandler
$response->setBody($record['formatted']);
$response->output();
return false === $this->bubble;
return false === $this->getBubble();
}
}

View File

@ -16,19 +16,31 @@ class MonologErrorHandler implements ErrorHandler
* Set the PSR-3 logger to send errors & exceptions to
*
* @param LoggerInterface $logger
* @return $this
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
return $this;
}
/**
* Get the PSR-3 logger to send errors & exceptions to
*
* @return LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
public function start()
{
if (!$this->logger) {
if (!$this->getLogger()) {
throw new \InvalidArgumentException("No Logger property passed to MonologErrorHandler."
. "Is your Injector config correct?");
}
MonologHandler::register($this->logger);
MonologHandler::register($this->getLogger());
}
}

View File

@ -713,37 +713,6 @@ MESSAGE
$this->transCreateField($table, $field, $spec_orig);
$this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
} elseif ($fieldValue != $specValue) {
// If enums/sets are being modified, then we need to fix existing data in the table.
// Update any records where the enum is set to a legacy value to be set to the default.
foreach (array('enum', 'set') as $enumtype) {
if (preg_match("/^$enumtype/i", $specValue)) {
$newStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $spec_orig);
$new = preg_split("/'\\s*,\\s*'/", $newStr);
$oldStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $fieldValue);
$old = preg_split("/'\\s*,\\s*'/", $oldStr);
$holder = array();
foreach ($old as $check) {
if (!in_array($check, $new)) {
$holder[] = $check;
}
}
if (count($holder)) {
$default = explode('default ', $spec_orig);
$default = $default[1];
$query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
for ($i = 0; $i + 1 < count($holder); $i++) {
$query .= "'{$holder[$i]}', ";
}
$query .= "'{$holder[$i]}')";
$this->query($query);
$amount = $this->database->affectedRows();
$this->alterationMessage("Changed $amount rows to default value of field $field"
. " (Value: $default)");
}
}
}
$this->transAlterField($table, $field, $spec_orig);
$this->alterationMessage(
"Field $table.$field: changed to $specValue <i class=\"build-info-before\">(from {$fieldValue})</i>",

View File

@ -583,6 +583,17 @@ abstract class Database
*/
abstract public function supportsTransactions();
/**
* Does this database support savepoints in transactions
* By default it is assumed that they don't unless they are explicitly enabled.
*
* @return boolean Flag indicating support for savepoints in transactions
*/
public function supportsSavepoints()
{
return false;
}
/**
* Invoke $callback within a transaction
*

View File

@ -21,7 +21,7 @@ use Exception;
* You are advised to backup your tables if changing settings on an existing database
* `connection_charset` and `charset` should be equal, similarly so should `connection_collation` and `collation`
*/
class MySQLDatabase extends Database
class MySQLDatabase extends Database implements TransactionManager
{
use Configurable;
@ -49,6 +49,13 @@ class MySQLDatabase extends Database
*/
private static $charset = 'utf8';
/**
* Cache for getTransactionManager()
*
* @var TransactionManager
*/
private $transactionManager = null;
/**
* Default collation
*
@ -57,11 +64,6 @@ class MySQLDatabase extends Database
*/
private static $collation = 'utf8_general_ci';
/**
* @var bool
*/
protected $transactionNesting = 0;
public function connect($parameters)
{
// Ensure that driver is available (required by PDO)
@ -298,73 +300,64 @@ class MySQLDatabase extends Database
return $list;
}
/**
* Returns the TransactionManager to handle transactions for this database.
*
* @return TransactionManager
*/
protected function getTransactionManager()
{
if (!$this->transactionManager) {
// PDOConnector providers this
if ($this->connector instanceof TransactionManager) {
$this->transactionManager = new NestedTransactionManager($this->connector);
// Direct database access does not
} else {
$this->transactionManager = new NestedTransactionManager(new MySQLTransactionManager($this));
}
}
return $this->transactionManager;
}
public function supportsTransactions()
{
return true;
}
public function supportsSavepoints()
{
return $this->getTransactionManager()->supportsSavepoints();
}
public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
{
if ($this->transactionNesting > 0) {
$this->transactionSavepoint('NESTEDTRANSACTION' . $this->transactionNesting);
} else {
// This sets the isolation level for the NEXT transaction, not the current one.
if ($transactionMode) {
$this->query('SET TRANSACTION ' . $transactionMode);
}
$this->query('START TRANSACTION');
if ($sessionCharacteristics) {
$this->query('SET SESSION TRANSACTION ' . $sessionCharacteristics);
}
}
++$this->transactionNesting;
$this->getTransactionManager()->transactionStart($transactionMode, $sessionCharacteristics);
}
public function transactionSavepoint($savepoint)
{
$this->query("SAVEPOINT $savepoint");
$this->getTransactionManager()->transactionSavepoint($savepoint);
}
public function transactionRollback($savepoint = false)
{
// Named transaction
if ($savepoint) {
$this->query('ROLLBACK TO ' . $savepoint);
return true;
}
// Fail if transaction isn't available
if (!$this->transactionNesting) {
return false;
}
--$this->transactionNesting;
if ($this->transactionNesting > 0) {
$this->transactionRollback('NESTEDTRANSACTION' . $this->transactionNesting);
} else {
$this->query('ROLLBACK');
}
return true;
return $this->getTransactionManager()->transactionRollback($savepoint);
}
public function transactionDepth()
{
return $this->transactionNesting;
return $this->getTransactionManager()->transactionDepth();
}
public function transactionEnd($chain = false)
{
// Fail if transaction isn't available
if (!$this->transactionNesting) {
return false;
$result = $this->getTransactionManager()->transactionEnd();
if ($chain) {
Deprecation::notice('4.4', '$chain argument is deprecated');
return $this->getTransactionManager()->transactionStart();
}
--$this->transactionNesting;
if ($this->transactionNesting <= 0) {
$this->transactionNesting = 0;
$this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN');
}
return true;
return $result;
}
/**
@ -372,6 +365,12 @@ class MySQLDatabase extends Database
*/
protected function resetTransactionNesting()
{
// Check whether to use a connector's built-in transaction methods
if ($this->connector instanceof TransactionalDBConnector) {
if ($this->transactionNesting > 0) {
$this->connector->transactionRollback();
}
}
$this->transactionNesting = 0;
}
@ -495,8 +494,9 @@ class MySQLDatabase extends Database
{
$id = $this->getLockIdentifier($name);
// MySQL auto-releases existing locks on subsequent GET_LOCK() calls,
// in contrast to PostgreSQL and SQL Server who stack the locks.
// MySQL 5.7.4 and below auto-releases existing locks on subsequent GET_LOCK() calls.
// MySQL 5.7.5 and newer allow multiple locks per sessions even with the same name.
// https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
return (bool) $this->query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value();
}

View File

@ -0,0 +1,100 @@
<?php
namespace SilverStripe\ORM\Connect;
use SilverStripe\Dev\Deprecation;
/**
* TransactionManager that executes MySQL-compatible transaction control queries
*/
class MySQLTransactionManager implements TransactionManager
{
protected $dbConn;
protected $inTransaction = false;
public function __construct(Database $dbConn)
{
$this->dbConn = $dbConn;
}
public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
{
if ($transactionMode || $sessionCharacteristics) {
Deprecation::notice(
'4.4',
'$transactionMode and $sessionCharacteristics are deprecated and will be removed in SS5'
);
}
if ($this->inTransaction) {
throw new DatabaseException(
"Already in transaction, can't start another. Consider decorating with NestedTransactionManager."
);
}
// This sets the isolation level for the NEXT transaction, not the current one.
if ($transactionMode) {
$this->dbConn->query('SET TRANSACTION ' . $transactionMode);
}
$this->dbConn->query('START TRANSACTION');
if ($sessionCharacteristics) {
$this->dbConn->query('SET SESSION TRANSACTION ' . $sessionCharacteristics);
}
$this->inTransaction = true;
return true;
}
public function transactionEnd($chain = false)
{
if (!$this->inTransaction) {
throw new DatabaseException("Not in transaction, can't end.");
}
if ($chain) {
user_error(
"transactionEnd() chain argument no longer implemented. Use NestedTransactionManager",
E_USER_WARNING
);
}
$this->dbConn->query('COMMIT');
$this->inTransaction = false;
return true;
}
public function transactionRollback($savepoint = null)
{
if (!$this->inTransaction) {
throw new DatabaseException("Not in transaction, can't roll back.");
}
if ($savepoint) {
$this->dbConn->query("ROLLBACK TO SAVEPOINT $savepoint");
} else {
$this->dbConn->query('ROLLBACK');
$this->inTransaction = false;
}
return true;
}
public function transactionSavepoint($savepoint)
{
$this->dbConn->query("SAVEPOINT $savepoint");
}
public function transactionDepth()
{
return (int)$this->inTransaction;
}
public function supportsSavepoints()
{
return true;
}
}

View File

@ -78,6 +78,18 @@ class MySQLiConnector extends DBConnector
$this->dbConn = mysqli_init();
// Use native types (MysqlND only)
if (defined('MYSQLI_OPT_INT_AND_FLOAT_NATIVE')) {
$this->dbConn->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
// The alternative is not ideal, throw a notice-level error
} else {
user_error(
'mysqlnd PHP library is not available, numeric values will be fetched from the DB as strings',
E_USER_NOTICE
);
}
// Set SSL parameters if they exist. All parameters are required.
if (array_key_exists('ssl_key', $parameters) &&
array_key_exists('ssl_cert', $parameters) &&

View File

@ -0,0 +1,127 @@
<?php
namespace SilverStripe\ORM\Connect;
/**
* TransactionManager decorator that adds virtual nesting support.
* Because this is managed in PHP and not the database, it has the following limitations:
* - Committing a nested transaction won't change anything until the parent transaction is committed
* - Rolling back a nested transaction means that the parent transaction must be rolled backed
*
* DBAL describes this behaviour nicely in their docs: https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/transactions.html#transaction-nesting
*/
class NestedTransactionManager implements TransactionManager
{
/**
* @var int
*/
protected $transactionNesting = 0;
/**
* @var TransactionManager
*/
protected $child;
/**
* Set to true if all transactions must roll back to the parent
* @var boolean
*/
protected $mustRollback = false;
/**
* Create a NestedTransactionManager
* @param TransactionManager $child The transaction manager that will handle the topmost transaction
*/
public function __construct(TransactionManager $child)
{
$this->child = $child;
}
/**
* Start a transaction
* @throws DatabaseException on failure
* @return bool True on success
*/
public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
{
if ($this->transactionNesting <= 0) {
$this->transactionNesting = 1;
$this->child->transactionStart($transactionMode, $sessionCharacteristics);
} else {
if ($this->child->supportsSavepoints()) {
$this->child->transactionSavepoint("nesting" . $this->transactionNesting);
}
$this->transactionNesting++;
}
}
public function transactionEnd($chain = false)
{
if ($this->mustRollback) {
throw new DatabaseException("Child transaction was rolled back, so parent can't be committed");
}
if ($this->transactionNesting < 1) {
throw new DatabaseException("Not within a transaction, so can't commit");
}
$this->transactionNesting--;
if ($this->transactionNesting === 0) {
$this->child->transactionEnd();
}
if ($chain) {
return $this->transactionStart();
}
}
public function transactionRollback($savepoint = null)
{
if ($this->transactionNesting < 1) {
throw new DatabaseException("Not within a transaction, so can't roll back");
}
if ($savepoint) {
return $this->child->transactionRollback($savepoint);
}
$this->transactionNesting--;
if ($this->transactionNesting === 0) {
$this->child->transactionRollback();
$this->mustRollback = false;
} else {
if ($this->child->supportsSavepoints()) {
$this->child->transactionRollback("nesting" . $this->transactionNesting);
$this->mustRollback = false;
// Without savepoints, parent transactions must roll back if a child one has
} else {
$this->mustRollback = true;
}
}
}
/**
* Return the depth of the transaction.
*
* @return int
*/
public function transactionDepth()
{
return $this->transactionNesting;
}
public function transactionSavepoint($savepoint)
{
return $this->child->transactionSavepoint($savepoint);
}
public function supportsSavepoints()
{
return $this->child->supportsSavepoints();
}
}

View File

@ -10,7 +10,7 @@ use InvalidArgumentException;
/**
* PDO driver database connector
*/
class PDOConnector extends DBConnector
class PDOConnector extends DBConnector implements TransactionManager
{
/**
@ -21,6 +21,15 @@ class PDOConnector extends DBConnector
*/
private static $emulate_prepare = false;
/**
* Should we return everything as a string in order to allow transaction savepoints?
* This preserves the behaviour of <= 4.3, including some bugs.
*
* @config
* @var boolean
*/
private static $legacy_types = false;
/**
* Default strong SSL cipher to be used
*
@ -64,6 +73,18 @@ class PDOConnector extends DBConnector
*/
protected $cachedStatements = array();
/**
* Driver
* @var string
*/
protected $driver = null;
/*
* Is a transaction currently active?
* @var bool
*/
protected $inTransaction = false;
/**
* Flush all prepared statements
*/
@ -113,10 +134,11 @@ class PDOConnector extends DBConnector
{
$this->flushStatements();
// Build DSN string
// Note that we don't select the database here until explicitly
// requested via selectDatabase
$driver = $parameters['driver'] . ":";
$this->driver = $parameters['driver'];
// Build DSN string
$dsn = array();
// Typically this is false, but some drivers will request this
@ -195,13 +217,23 @@ class PDOConnector extends DBConnector
$options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default');
}
if (self::is_emulate_prepare()) {
if (static::config()->get('legacy_types')) {
$options[PDO::ATTR_STRINGIFY_FETCHES] = true;
$options[PDO::ATTR_EMULATE_PREPARES] = true;
} else {
// Set emulate prepares (unless null / default)
$isEmulatePrepares = self::is_emulate_prepare();
if (isset($isEmulatePrepares)) {
$options[PDO::ATTR_EMULATE_PREPARES] = (bool)$isEmulatePrepares;
}
// Disable stringified fetches
$options[PDO::ATTR_STRINGIFY_FETCHES] = false;
}
// May throw a PDOException if fails
$this->pdoConnection = new PDO(
$driver . implode(';', $dsn),
$this->driver . ':' . implode(';', $dsn),
empty($parameters['username']) ? '' : $parameters['username'],
empty($parameters['password']) ? '' : $parameters['password'],
$options
@ -213,6 +245,18 @@ class PDOConnector extends DBConnector
}
}
/**
* Return the driver for this connector
* E.g. 'mysql', 'sqlsrv', 'pgsql'
*
* @return string
*/
public function getDriver()
{
return $this->driver;
}
public function getVersion()
{
return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION);
@ -383,7 +427,7 @@ class PDOConnector extends DBConnector
} elseif ($statement) {
// Count and return results
$this->rowCount = $statement->rowCount();
return new PDOQuery($statement);
return new PDOQuery($statement, $this);
}
// Ensure statement is closed
@ -468,4 +512,60 @@ class PDOConnector extends DBConnector
{
return $this->databaseName && $this->pdoConnection;
}
public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
{
$this->inTransaction = true;
if ($transactionMode) {
$this->query("SET TRANSACTION $transactionMode");
}
if ($this->pdoConnection->beginTransaction()) {
if ($sessionCharacteristics) {
$this->query("SET SESSION CHARACTERISTICS AS TRANSACTION $sessionCharacteristics");
}
return true;
}
return false;
}
public function transactionEnd()
{
$this->inTransaction = false;
return $this->pdoConnection->commit();
}
public function transactionRollback($savepoint = null)
{
if ($savepoint) {
if ($this->supportsSavepoints()) {
$this->exec("ROLLBACK TO SAVEPOINT $savepoint");
} else {
throw new DatabaseException("Savepoints not supported on this PDO connection");
}
}
$this->inTransaction = false;
return $this->pdoConnection->rollBack();
}
public function transactionDepth()
{
return (int)$this->inTransaction;
}
public function transactionSavepoint($savepoint = null)
{
if ($this->supportsSavepoints()) {
$this->exec("SAVEPOINT $savepoint");
} else {
throw new DatabaseException("Savepoints not supported on this PDO connection");
}
}
public function supportsSavepoints()
{
return static::config()->get('legacy_types');
}
}

View File

@ -22,16 +22,54 @@ class PDOQuery extends Query
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
* @param PDOStatement $statement The internal PDOStatement containing the results
*/
public function __construct(PDOStatement $statement)
public function __construct(PDOStatement $statement, PDOConnector $conn)
{
$this->statement = $statement;
// Since no more than one PDOStatement for any one connection can be safely
// traversed, each statement simply requests all rows at once for safety.
// This could be re-engineered to call fetchAll on an as-needed basis
$this->results = $statement->fetchAll(PDO::FETCH_ASSOC);
// Special case for Postgres
if ($conn->getDriver() == 'pgsql') {
$this->results = $this->fetchAllPgsql($statement);
} else {
$this->results = $statement->fetchAll(PDO::FETCH_ASSOC);
}
$statement->closeCursor();
}
/**
* Fetch a record form the statement with its type data corrected
* Necessary to fix float data retrieved from PGSQL
* Returns data as an array of maps
* @return array
*/
protected function fetchAllPgsql($statement)
{
$columnCount = $statement->columnCount();
$columnMeta = [];
for ($i = 0; $i<$columnCount; $i++) {
$columnMeta[$i] = $statement->getColumnMeta($i);
}
// Re-map fetched data using columnMeta
return array_map(
function ($rowArray) use ($columnMeta) {
$row = [];
foreach ($columnMeta as $i => $meta) {
// Coerce floats from string to float
// PDO PostgreSQL fails to do this
if (isset($meta['native_type']) && strpos($meta['native_type'], 'float') === 0) {
$rowArray[$i] = (float)$rowArray[$i];
}
$row[$meta['name']] = $rowArray[$i];
}
return $row;
},
$statement->fetchAll(PDO::FETCH_NUM)
);
}
public function seek($row)
{
$this->rowNum = $row - 1;

View File

@ -6,12 +6,24 @@ use SilverStripe\Core\Convert;
use Iterator;
/**
* Abstract query-result class.
* Abstract query-result class. A query result provides an iterator that returns a map for each record of a query
* result.
*
* The map should be keyed by the column names, and the values should use the following types:
*
* - boolean returned as integer 1 or 0 (to ensure consistency with MySQL that doesn't have native booleans)
* - integer types returned as integers
* - floating point / decimal types returned as floats
* - strings returned as strings
* - dates / datetimes returned as strings
*
* Note that until SilverStripe 4.3, bugs meant that strings were used for every column type.
*
* Once again, this should be subclassed by an actual database implementation. It will only
* ever be constructed by a subclass of SS_Database. The result of a database query - an iteratable object
* that's returned by DB::SS_Query
*
* Primarily, the SS_Query class takes care of the iterator plumbing, letting the subclasses focusing
* Primarily, the Query class takes care of the iterator plumbing, letting the subclasses focusing
* on providing the specific data-access methods that are required: {@link nextRecord()}, {@link numRecords()}
* and {@link seek()}
*/

View File

@ -0,0 +1,67 @@
<?php
namespace SilverStripe\ORM\Connect;
/**
* Represents an object that is capable of controlling transactions.
*
* The TransactionManager might be the database connection itself, calling queries to orchestrate
* transactions, or a connector such as the PDOConnector.
*
* Generally speaking you should rely on your Database object to manage the creation of a TansactionManager
* for you; unless you are building new database connectors this should be treated as an internal API.
*/
interface TransactionManager
{
/**
* Start a prepared transaction
*
* @param string|boolean $transactionMode Transaction mode, or false to ignore. Deprecated and will be removed in SS5.
* @param string|boolean $sessionCharacteristics Session characteristics, or false to ignore. Deprecated and will be removed in SS5.
* @throws DatabaseException on failure
* @return bool True on success
*/
public function transactionStart($transactionMode = false, $sessionCharacteristics = false);
/**
* Complete a transaction
*
* @throws DatabaseException on failure
* @return bool True on success
*/
public function transactionEnd();
/**
* Roll-back a transaction
*
* @param string $savepoint If set, roll-back to the named savepoint
* @throws DatabaseException on failure
* @return bool True on success
*/
public function transactionRollback($savepoint = null);
/**
* Create a new savepoint
*
* @param string $savepoint The savepoint name
* @throws DatabaseException on failure
*/
public function transactionSavepoint($savepoint);
/**
* Return the depth of the transaction
* For unnested transactions returns 1 while in a transaction, 0 otherwise
*
* @return int
*/
public function transactionDepth();
/**
* Return true if savepoints are supported by this transaction manager.
* Savepoints aren't supported by all database connectors (notably PDO doesn't support them)
* and should be used with caution.
*
* @return boolean
*/
public function supportsSavepoints();
}

View File

@ -19,6 +19,7 @@ use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\ORM\Connect\MySQLSchemaManager;
use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField;
@ -188,14 +189,23 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @var array
*/
private $changed;
private $changed = [];
/**
* A flag to indicate that a "strict" change of the entire record been forced
* Use {@link getChangedFields()} and {@link isChanged()} to inspect
* the changed state.
*
* @var boolean
*/
private $changeForced = false;
/**
* The database record (in the same format as $record), before
* any changes.
* @var array
*/
protected $original;
protected $original = [];
/**
* Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
@ -387,7 +397,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
// prevent populateDefaults() and setField() from marking overwritten defaults as changed
$this->changed = array();
$this->changed = [];
$this->changeForced = false;
}
/**
@ -1107,8 +1118,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Forces the record to think that all its data has changed.
* Doesn't write to the database. Only sets fields as changed
* if they are not already marked as changed.
* Doesn't write to the database. Force-change preseved until
* next write. Existing CHANGE_VALUE or CHANGE_STRICT values
* are preserved.
*
* @return $this
*/
@ -1116,28 +1128,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
{
// Ensure lazy fields loaded
$this->loadLazyFields();
$fields = static::getSchema()->fieldSpecs(static::class);
// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
$fieldNames = array_unique(array_merge(
array_keys($this->record),
array_keys($fields)
));
foreach ($fieldNames as $fieldName) {
if (!isset($this->changed[$fieldName])) {
$this->changed[$fieldName] = self::CHANGE_STRICT;
}
// Populate the null values in record so that they actually get written
// Populate the null values in record so that they actually get written
foreach (array_keys(static::getSchema()->fieldSpecs(static::class)) as $fieldName) {
if (!isset($this->record[$fieldName])) {
$this->record[$fieldName] = null;
}
}
// @todo Find better way to allow versioned to write a new version after forceChange
if ($this->isChanged('Version')) {
unset($this->changed['Version']);
}
$this->changeForced = true;
return $this;
}
@ -1374,11 +1374,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$table = $schema->tableName($class);
$manipulation[$table] = array();
$changed = $this->getChangedFields();
// Extract records for this table
foreach ($this->record as $fieldName => $fieldValue) {
// we're not attempting to reset the BaseTable->ID
// Ignore unchanged fields or attempts to reset the BaseTable->ID
if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
if (empty($changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
continue;
}
@ -1520,7 +1522,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// If there's any relations that couldn't be saved before, save them now (we have an ID here)
$this->writeRelations();
$this->onAfterWrite();
$this->changed = array();
// Reset isChanged data
// DBComposites properly bound to the parent record will also have their isChanged value reset
$this->changed = [];
$this->changeForced = false;
$this->original = $this->record;
} else {
if ($showDebug) {
Debug::message("no changes for DataObject");
@ -2051,6 +2058,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$query->setQueryParam('Component.ExtraFields', $extraFields);
});
// If we have a default sort set for our "join" then we should overwrite any default already set.
$joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
if (!empty($joinSort)) {
$result = $result->sort($joinSort);
}
$this->extend('updateManyManyComponents', $result);
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
@ -2481,7 +2494,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
/**
* Return the fields that have changed.
* Return the fields that have changed since the last write.
*
* The change level affects what the functions defines as "changed":
* - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
@ -2515,13 +2528,25 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
// If change was forced, then derive change data from $this->record
if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
$changed = array_combine(
array_keys($this->record),
array_fill(0, count($this->record), self::CHANGE_STRICT)
);
// @todo Find better way to allow versioned to write a new version after forceChange
unset($changed['Version']);
} else {
$changed = $this->changed;
}
if (is_array($databaseFieldsOnly)) {
$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
$fields = array_intersect_key($changed, array_flip($databaseFieldsOnly));
} elseif ($databaseFieldsOnly) {
$fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
$fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
$fields = array_intersect_key($changed, $fieldsSpecs);
} else {
$fields = $this->changed;
$fields = $changed;
}
// Filter the list to those of a certain change level
@ -2622,22 +2647,25 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
// if a field is not existing or has strictly changed
if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
if (!isset($this->original[$fieldName]) || $this->original[$fieldName] !== $val) {
// TODO Add check for php-level defaults which are not set in the db
// TODO Add check for hidden input-fields (readonly) which are not set in the db
// At the very least, the type has changed
$this->changed[$fieldName] = self::CHANGE_STRICT;
if ((!isset($this->record[$fieldName]) && $val)
|| (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
if ((!isset($this->original[$fieldName]) && $val)
|| (isset($this->original[$fieldName]) && $this->original[$fieldName] != $val)
) {
// Value has changed as well, not just the type
$this->changed[$fieldName] = self::CHANGE_VALUE;
}
// Value is always saved back when strict check succeeds.
$this->record[$fieldName] = $val;
// Value has been restored to its original, remove any record of the change
} elseif (isset($this->changed[$fieldName])) {
unset($this->changed[$fieldName]);
}
// Value is saved regardless, since the change detection relates to the last write
$this->record[$fieldName] = $val;
}
return $this;
}
@ -3170,7 +3198,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public static function reset()
{
// @todo Decouple these
DBClassName::clear_classname_cache();
DBEnum::flushCache();
ClassInfo::reset_db_cache();
static::getSchema()->reset();
self::$_cache_get_one = array();

View File

@ -8,6 +8,7 @@ use LogicException;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\TestOnly;
@ -127,7 +128,7 @@ class DataObjectSchema
$tables = $this->getTableNames();
$class = ClassInfo::class_name($class);
if (isset($tables[$class])) {
return $tables[$class];
return Convert::raw2sql($tables[$class]);
}
return null;
}

View File

@ -6,6 +6,7 @@ use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\Dev\Deprecation;
/**
* Represents a classname selector, which respects obsolete clasess.
@ -29,14 +30,6 @@ class DBClassName extends DBEnum
*/
protected $record = null;
/**
* Classname spec cache for obsolete classes. The top level keys are the table, each of which contains
* nested arrays with keys mapped to field names. The values of the lowest level array are the classnames
*
* @var array
*/
protected static $classname_cache = array();
private static $index = true;
/**
@ -45,7 +38,8 @@ class DBClassName extends DBEnum
*/
public static function clear_classname_cache()
{
self::$classname_cache = array();
Deprecation::notice('4.3', 'Call DBEnum::flushCache() instead');
DBEnum::flushCache();
}
/**
@ -149,47 +143,6 @@ class DBClassName extends DBEnum
return array_values($classNames);
}
/**
* Get the list of classnames, including obsolete classes.
*
* If table or name are not set, or if it is not a valid field on the given table,
* then only known classnames are returned.
*
* Values cached in this method can be cleared via `DBClassName::clear_classname_cache();`
*
* @return array
*/
public function getEnumObsolete()
{
// Without a table or field specified, we can only retrieve known classes
$table = $this->getTable();
$name = $this->getName();
if (empty($table) || empty($name)) {
return $this->getEnum();
}
// Ensure the table level cache exists
if (empty(self::$classname_cache[$table])) {
self::$classname_cache[$table] = array();
}
// Check existing cache
if (!empty(self::$classname_cache[$table][$name])) {
return self::$classname_cache[$table][$name];
}
// Get all class names
$classNames = $this->getEnum();
if (DB::get_schema()->hasField($table, $name)) {
$existing = DB::query("SELECT DISTINCT \"{$name}\" FROM \"{$table}\"")->column();
$classNames = array_unique(array_merge($classNames, $existing));
}
// Cache and return
self::$classname_cache[$table][$name] = $classNames;
return $classNames;
}
public function setValue($value, $record = null, $markChanged = true)
{
parent::setValue($value, $record, $markChanged);

View File

@ -36,6 +36,12 @@ abstract class DBComposite extends DBField
*/
private static $composite_db = array();
/**
* Marker as to whether this record has changed
* Only used when deference to the parent object isn't possible
*/
protected $isChanged = false;
/**
* Either the parent dataobject link, or a record of saved values for each field
*
@ -113,6 +119,10 @@ abstract class DBComposite extends DBField
}
/**
* Returns true if this composite field has changed.
* For fields bound to a DataObject, this will be cleared when the DataObject is written.
*/
public function isChanged()
{
// When unbound, use the local changed flag

View File

@ -20,7 +20,7 @@ class DBEnum extends DBString
*
* @var array
*/
protected $enum = array();
protected $enum = [];
/**
* Default value
@ -31,6 +31,22 @@ class DBEnum extends DBString
private static $default_search_filter_class = 'ExactMatchFilter';
/**
* Internal cache for obsolete enum values. The top level keys are the table, each of which contains
* nested arrays with keys mapped to field names. The values of the lowest level array are the enum values
*
* @var array
*/
protected static $enum_cache = [];
/**
* Clear all cached enum values.
*/
public static function flushCache()
{
self::$enum_cache = [];
}
/**
* Create a new Enum field, which is a value within a defined set, with an optional default.
*
@ -88,7 +104,7 @@ class DBEnum extends DBString
$parts = array(
'datatype' => 'enum',
'enums' => $this->getEnum(),
'enums' => $this->getEnumObsolete(),
'character set' => $charset,
'collate' => $collation,
'default' => $this->getDefault(),
@ -173,6 +189,48 @@ class DBEnum extends DBString
return $this->enum;
}
/**
* Get the list of enum values, including obsolete values still present in the database
*
* If table or name are not set, or if it is not a valid field on the given table,
* then only known enum values are returned.
*
* Values cached in this method can be cleared via `DBEnum::flushCache();`
*
* @return array
*/
public function getEnumObsolete()
{
// Without a table or field specified, we can only retrieve known enum values
$table = $this->getTable();
$name = $this->getName();
if (empty($table) || empty($name)) {
return $this->getEnum();
}
// Ensure the table level cache exists
if (empty(self::$enum_cache[$table])) {
self::$enum_cache[$table] = array();
}
// Check existing cache
if (!empty(self::$enum_cache[$table][$name])) {
return self::$enum_cache[$table][$name];
}
// Get all enum values
$enumValues = $this->getEnum();
if (DB::get_schema()->hasField($table, $name)) {
$existing = DB::query("SELECT DISTINCT \"{$name}\" FROM \"{$table}\"")->column();
$enumValues = array_unique(array_merge($enumValues, $existing));
}
// Cache and return
self::$enum_cache[$table][$name] = $enumValues;
return $enumValues;
}
/**
* Set enum options
*

View File

@ -173,8 +173,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable,
*/
public function sort()
{
$args = func_get_args();
return call_user_func_array(array($this->list, 'sort'), $args);
return $this->list->sort(...func_get_args());
}
public function canFilterBy($by)
@ -192,8 +191,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable,
*/
public function filter()
{
$args = func_get_args();
return call_user_func_array(array($this->list, 'filter'), $args);
return $this->list->filter(...func_get_args());
}
/**
@ -220,7 +218,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable,
*/
public function filterAny()
{
return call_user_func_array(array($this->list, __FUNCTION__), func_get_args());
return $this->list->filterAny(...func_get_args());
}
/**
@ -242,7 +240,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable,
}
$output = ArrayList::create();
foreach ($this->list as $item) {
if (call_user_func($callback, $item, $this->list)) {
if ($callback($item, $this->list)) {
$output->push($item);
}
}
@ -286,8 +284,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable,
*/
public function exclude()
{
$args = func_get_args();
return call_user_func_array(array($this->list, 'exclude'), $args);
return $this->list->exclude(...func_get_args());
}
public function debug()

View File

@ -254,13 +254,6 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
);
}
// Set a default sort from the join model if available and nothing is already set
if (empty($sqlSelect->getOrderBy())
&& $sort = Config::inst()->get($this->getJoinClass(), 'default_sort')
) {
$sqlSelect->setOrderBy($sort);
}
// Apply join and record sql for later insertion (at end of replacements)
// By using a string placeholder $$_SUBQUERY_$$ we protect field/table rewrites from interfering twice
// on the already-finalised inner list

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Security;
use Error;
use Exception;
use SilverStripe\Dev\Deprecation;
/**
* Convenience class for generating cryptographically secure pseudo-random strings/tokens
@ -13,9 +14,12 @@ class RandomGenerator
/**
* @return string A 128-character, randomly generated ASCII string
* @throws Exception If no suitable CSPRNG is installed
* @deprecated 4.4.0:5.0.0
*/
public function generateEntropy()
{
Deprecation::notice('4.4', __METHOD__ . ' has been deprecated. Use random_bytes instead');
try {
return bin2hex(random_bytes(64));
} catch (Error $e) {
@ -38,9 +42,10 @@ class RandomGenerator
*
* @param string $algorithm Any identifier listed in hash_algos() (Default: whirlpool)
* @return string Returned length will depend on the used $algorithm
* @throws Exception When there is no valid source of CSPRNG
*/
public function randomToken($algorithm = 'whirlpool')
{
return hash($algorithm, $this->generateEntropy());
return hash($algorithm, random_bytes(64));
}
}

View File

@ -35,7 +35,11 @@ abstract class HTMLValue extends ViewableData
*/
public function getContent()
{
$doc = clone $this->getDocument();
$document = $this->getDocument();
if (!$document) {
return '';
}
$doc = clone $document;
$xp = new DOMXPath($doc);
// If there's no body, the content is empty string

View File

@ -18,6 +18,7 @@ use SilverStripe\Core\Path;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\Deprecation;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBField;
class Requirements_Backend
{
@ -1004,13 +1005,23 @@ class Requirements_Backend
$files = array();
$candidates = array(
'en.js',
'en_US.js',
i18n::getData()->langFromLocale(i18n::config()->get('default_locale')) . '.js',
i18n::config()->get('default_locale') . '.js',
i18n::getData()->langFromLocale(i18n::get_locale()) . '.js',
i18n::get_locale() . '.js',
'en',
'en_US',
i18n::getData()->langFromLocale(i18n::config()->get('default_locale')),
i18n::config()->get('default_locale'),
i18n::getData()->langFromLocale(i18n::get_locale()),
i18n::get_locale(),
strtolower(DBField::create_field('Locale', i18n::get_locale())->RFC1766()),
strtolower(DBField::create_field('Locale', i18n::config()->get('default_locale'))->RFC1766())
);
$candidates = array_map(
function ($candidate) {
return $candidate . '.js';
},
$candidates
);
foreach ($candidates as $candidate) {
$relativePath = Path::join($langDir, $candidate);
$absolutePath = Director::getAbsFile($relativePath);

View File

@ -114,11 +114,11 @@ class EmbedShortcodeProvider implements ShortcodeHandler
if (empty($arguments['width']) && $embed->getWidth()) {
$arguments['width'] = $embed->getWidth();
}
return self::videoEmbed($arguments, $embed->getCode());
return static::videoEmbed($arguments, $embed->getCode());
case 'link':
return self::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
return static::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
case 'photo':
return self::photoEmbed($arguments, $embed->getUrl());
return static::photoEmbed($arguments, $embed->getUrl());
default:
return null;
}

View File

@ -27,6 +27,7 @@ use SilverStripe\Core\TempFolder;
* - PUBLIC_PATH: Absolute path to webroot, e.g. "/var/www/project/public"
* - THIRDPARTY_DIR: Path relative to webroot, e.g. "framework/thirdparty"
* - THIRDPARTY_PATH: Absolute filepath, e.g. "/var/www/my-webroot/framework/thirdparty"
* - RESOURCES_DIR: Name of the directory where vendor assets will be exposed, e.g. "_ressources"
*/
require_once __DIR__ . '/functions.php';
@ -198,3 +199,18 @@ if (!defined('TEMP_PATH')) {
if (!defined('TEMP_FOLDER')) {
define('TEMP_FOLDER', TEMP_PATH);
}
// Define the Ressource Dir constant that will be use to exposed vendor assets
if (!defined('RESOURCES_DIR')) {
$project = new SilverStripe\Core\Manifest\Module(BASE_PATH, BASE_PATH);
$resourcesDir = $project->getResourcesDir() ?: 'resources';
if (preg_match('/^[_\-a-z0-9]+$/i', $resourcesDir)) {
define('RESOURCES_DIR', $resourcesDir);
} else {
throw new LogicException(sprintf(
'Resources dir error: "%s" is not a valid resources directory name. Update the ' .
'`extra.resources-dir` key in your composer.json file',
$resourcesDir
));
}
}

View File

@ -1,6 +1,6 @@
<% loop $FieldList %>
<% if $ColumnCount %>
<div class="column-{$ColumnCount} $FirstLast">
<% if $Up.ColumnCount %>
<div class="column-{$Up.ColumnCount} $FirstLast">
$FieldHolder
</div>
<% else %>

View File

@ -4,8 +4,8 @@
<% end_if %>
<% loop $FieldList %>
<% if $ColumnCount %>
<div class="column-{$ColumnCount} $FirstLast">
<% if $Up.ColumnCount %>
<div class="column-{$Up.ColumnCount} $FirstLast">
$SmallFieldHolder
</div>
<% else %>

View File

@ -7,7 +7,7 @@
<body class="cms cms-security fill-height">
<% with $Form %>
<% if $Message %>
<div class="cms-security__container__error message $MessageType">
<div class="cms-security__container__error alert $AlertType">
<p id="{$FormName}_error">$Message</p>
</div>
<% end_if %>

View File

@ -58,7 +58,10 @@ class CmsUiContext implements Context
$timeoutMs = $this->getMainContext()->getAjaxTimeout();
$this->getSession()->wait(
$timeoutMs,
"document.getElementsByClassName('cms-content-loading-overlay').length == 0"
"(".
"document.getElementsByClassName('cms-content-loading-overlay').length +".
"document.getElementsByClassName('cms-loading-container').length".
") == 0"
);
}
@ -85,7 +88,13 @@ class CmsUiContext implements Context
*/
public function iShouldSeeAMessage($message)
{
$this->getMainContext()->assertElementContains('.message', $message);
$page = $this->getMainContext()->getSession()->getPage();
if ($page->find('css', '.message')) {
$this->getMainContext()->assertElementContains('.message', $message);
} else {
// Support for new Bootstrap alerts
$this->getMainContext()->assertElementContains('.alert', $message);
}
}
protected function getCmsTabsElement()

View File

@ -36,7 +36,7 @@ class SimpleResourceURLGeneratorTest extends SapphireTest
__DIR__ . '/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/file.js'
);
$this->assertEquals(
'/resources/basemodule/client/file.js?m=' . $mtime,
'/'. RESOURCES_DIR . '/basemodule/client/file.js?m=' . $mtime,
$generator->urlForResource('basemodule/client/file.js')
);
}
@ -49,7 +49,7 @@ class SimpleResourceURLGeneratorTest extends SapphireTest
__DIR__ . '/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css'
);
$this->assertEquals(
'/resources/vendor/silverstripe/mymodule/client/style.css?m=' . $mtime,
'/'. RESOURCES_DIR . '/vendor/silverstripe/mymodule/client/style.css?m=' . $mtime,
$generator->urlForResource('vendor/silverstripe/mymodule/client/style.css')
);
}
@ -72,7 +72,7 @@ class SimpleResourceURLGeneratorTest extends SapphireTest
);
$this->assertEquals(
'/resources/basemodule/client/file.js?m=' . $mtime,
'/'. RESOURCES_DIR . '/basemodule/client/file.js?m=' . $mtime,
$generator->urlForResource('basemodule/client/file.js')
);
}
@ -89,7 +89,7 @@ class SimpleResourceURLGeneratorTest extends SapphireTest
__DIR__ . '/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css'
);
$this->assertEquals(
'/resources/vendor/silverstripe/mymodule/client/style.css?m=' . $mtime,
'/'. RESOURCES_DIR . '/vendor/silverstripe/mymodule/client/style.css?m=' . $mtime,
$generator->urlForResource($module->getResource('client/style.css'))
);
}

View File

@ -2,12 +2,12 @@
namespace SilverStripe\Core\Tests;
use Exception;
use InvalidArgumentException;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\View\Parsers\URLSegmentFilter;
use stdClass;
use Exception;
use InvalidArgumentException;
/**
* Test various functions on the {@link Convert} class.
@ -133,7 +133,7 @@ class ConvertTest extends SapphireTest
$this->assertEquals(
"That's absolutely correct",
Convert::html2raw($val7),
"Single quotes are decoded correctly"
'Single quotes are decoded correctly'
);
$val8 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor ' . 'incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' . 'exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute ' . 'irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla ' . 'pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia ' . 'deserunt mollit anim id est laborum.';
@ -280,7 +280,7 @@ PHP
protected function assertEqualsQuoted($expected, $actual)
{
$message = sprintf(
"Expected \"%s\" but given \"%s\"",
'Expected "%s" but given "%s"',
addcslashes($expected, "\r\n"),
addcslashes($actual, "\r\n")
);
@ -296,8 +296,8 @@ PHP
foreach (array("\r\n", "\r", "\n") as $nl) {
// Base case: no action
$this->assertEqualsQuoted(
"Base case",
Convert::nl2os("Base case", $nl)
'Base case',
Convert::nl2os('Base case', $nl)
);
// Mixed formats
@ -426,7 +426,7 @@ XML
// Test without doctype validation
$expected = array(
'result' => array(
"Now include SOME_SUPER_LONG_STRING lots of times to expand the in-memory size of this XML structure",
'Now include SOME_SUPER_LONG_STRING lots of times to expand the in-memory size of this XML structure',
array(
'long' => array(
array(
@ -576,14 +576,15 @@ XML
public function memString2BytesProvider()
{
return [
['2048', (float)(2 * 1024)],
['2k', (float)(2 * 1024)],
['512M', (float)(512 * 1024 * 1024)],
['512MiB', (float)(512 * 1024 * 1024)],
['512 mbytes', (float)(512 * 1024 * 1024)],
['512 megabytes', (float)(512 * 1024 * 1024)],
['1024g', (float)(1024 * 1024 * 1024 * 1024)],
['1024G', (float)(1024 * 1024 * 1024 * 1024)]
'infinite' => ['-1', -1],
'integer' => ['2048', 2 * 1024],
'kilo' => ['2k', 2 * 1024],
'mega' => ['512M', 512 * 1024 * 1024],
'MiB' => ['512MiB', 512 * 1024 * 1024],
'mbytes' => ['512 mbytes', 512 * 1024 * 1024],
'megabytes' => ['512 megabytes', 512 * 1024 * 1024],
'giga' => ['1024g', 1024 * 1024 * 1024 * 1024],
'G' => ['1024G', 1024 * 1024 * 1024 * 1024]
];
}
@ -606,11 +607,11 @@ XML
{
return [
[200, '200B'],
[(2 * 1024), '2K'],
[(512 * 1024 * 1024), '512M'],
[(512 * 1024 * 1024 * 1024), '512G'],
[(512 * 1024 * 1024 * 1024 * 1024), '512T'],
[(512 * 1024 * 1024 * 1024 * 1024 * 1024), '512P']
[2 * 1024, '2K'],
[512 * 1024 * 1024, '512M'],
[512 * 1024 * 1024 * 1024, '512G'],
[512 * 1024 * 1024 * 1024 * 1024, '512T'],
[512 * 1024 * 1024 * 1024 * 1024 * 1024, '512P']
];
}

View File

@ -21,13 +21,12 @@ use SilverStripe\Core\Tests\Injector\InjectorTest\NeedsBothCirculars;
use SilverStripe\Core\Tests\Injector\InjectorTest\NewRequirementsBackend;
use SilverStripe\Core\Tests\Injector\InjectorTest\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());
}
}

View File

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

View File

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

View File

@ -42,7 +42,7 @@ class ModuleResourceTest extends SapphireTest
$resource->getPath()
);
$this->assertStringStartsWith(
'/basefolder/resources/module/client/script.js?m=',
'/basefolder/'. RESOURCES_DIR . '/module/client/script.js?m=',
$resource->getURL()
);
}
@ -60,7 +60,7 @@ class ModuleResourceTest extends SapphireTest
$resource->getPath()
);
$this->assertStringStartsWith(
'/basefolder/resources/vendor/silverstripe/modulec/client/script.js?m=',
'/basefolder/'. RESOURCES_DIR . '/vendor/silverstripe/modulec/client/script.js?m=',
$resource->getURL()
);
}
@ -80,7 +80,7 @@ class ModuleResourceTest extends SapphireTest
$resource->getPath()
);
$this->assertStringStartsWith(
'/basefolder/resources/vendor/silverstripe/modulec/client/script.js?m=',
'/basefolder/'. RESOURCES_DIR . '/vendor/silverstripe/modulec/client/script.js?m=',
$resource->getURL()
);
}

View File

@ -0,0 +1,24 @@
<?php
namespace SilverStripe\Core\Tests\Manifest;
use SilverStripe\Control\Director;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Dev\SapphireTest;
class ModuleTest extends SapphireTest
{
public function testUnsetResourcesDir()
{
$path = __DIR__ . '/fixtures/ss-projects/withoutCustomResourcesDir';
$module = new Module($path, $path);
$this->assertEquals('', $module->getResourcesDir());
}
public function testResourcesDir()
{
$path = __DIR__ . '/fixtures/ss-projects/withCustomResourcesDir';
$module = new Module($path, $path);
$this->assertEquals('customised-resources-dir', $module->getResourcesDir());
}
}

View File

@ -0,0 +1,29 @@
{
"name": "silverstripe/ss44",
"type": "silverstripe-project",
"description": "Fake project using SS 4.4",
"homepage": "https://www.silverstripe.org",
"license": "BSD-3-Clause",
"require": {
"silverstripe/recipe-cms": "4.4.x-dev as 4.4.0"
},
"extra": {
"project-files-installed": [
"app/.htaccess",
"app/_config.php",
"app/_config/mysite.yml",
"app/src/Page.php",
"app/src/PageController.php"
],
"public-files-installed": [
".htaccess",
"index.php",
"install-frameworkmissing.html",
"install.php",
"web.config"
],
"resources-dir": "customised-resources-dir"
},
"prefer-stable": true,
"minimum-stability": "dev"
}

View File

@ -0,0 +1,28 @@
{
"name": "silverstripe/ss44",
"type": "silverstripe-project",
"description": "Fake project using SS 4.4",
"homepage": "https://www.silverstripe.org",
"license": "BSD-3-Clause",
"require": {
"silverstripe/recipe-cms": "4.4.x-dev as 4.4.0"
},
"extra": {
"project-files-installed": [
"app/.htaccess",
"app/_config.php",
"app/_config/mysite.yml",
"app/src/Page.php",
"app/src/PageController.php"
],
"public-files-installed": [
".htaccess",
"index.php",
"install-frameworkmissing.html",
"install.php",
"web.config"
]
},
"prefer-stable": true,
"minimum-stability": "dev"
}

View File

@ -0,0 +1,187 @@
<?php
namespace SilverStripe\Core\Tests\Startup;
use SilverStripe\Core\Startup\ConfirmationTokenChain;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\Core\Startup\URLConfirmationToken;
use SilverStripe\Dev\SapphireTest;
class ConfirmationTokenChainTest extends SapphireTest
{
protected function getTokenRequiringReload($requiresReload = true, $extraMethods = [])
{
$methods = array_merge(['reloadRequired'], $extraMethods);
$mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods);
$mock->expects($this->any())
->method('reloadRequired')
->will($this->returnValue($requiresReload));
return $mock;
}
protected function getTokenRequiringReloadIfError($requiresReload = true, $extraMethods = [])
{
$methods = array_merge(['reloadRequired', 'reloadRequiredIfError'], $extraMethods);
$mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods);
$mock->expects($this->any())
->method('reloadRequired')
->will($this->returnValue(false));
$mock->expects($this->any())
->method('reloadRequiredIfError')
->will($this->returnValue($requiresReload));
return $mock;
}
public function testFilteredTokens()
{
$chain = new ConfirmationTokenChain();
$chain->pushToken($tokenRequiringReload = $this->getTokenRequiringReload());
$chain->pushToken($tokenNotRequiringReload = $this->getTokenRequiringReload(false));
$chain->pushToken($tokenRequiringReloadIfError = $this->getTokenRequiringReloadIfError());
$chain->pushToken($tokenNotRequiringReloadIfError = $this->getTokenRequiringReloadIfError(false));
$reflectionMethod = new \ReflectionMethod(ConfirmationTokenChain::class, 'filteredTokens');
$reflectionMethod->setAccessible(true);
$tokens = iterator_to_array($reflectionMethod->invoke($chain));
$this->assertContains($tokenRequiringReload, $tokens, 'Token requiring a reload was not returned');
$this->assertNotContains($tokenNotRequiringReload, $tokens, 'Token not requiring a reload was returned');
$this->assertContains($tokenRequiringReloadIfError, $tokens, 'Token requiring a reload on error was not returned');
$this->assertNotContains($tokenNotRequiringReloadIfError, $tokens, 'Token not requiring a reload on error was returned');
}
public function testSuppressionRequired()
{
$chain = new ConfirmationTokenChain();
$chain->pushToken($this->getTokenRequiringReload(false));
$this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required');
$chain = new ConfirmationTokenChain();
$chain->pushToken($this->getTokenRequiringReloadIfError(false));
$this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required');
$chain = new ConfirmationTokenChain();
$chain->pushToken($this->getTokenRequiringReload());
$this->assertTrue($chain->suppressionRequired(), 'Suppression not marked as required');
$chain = new ConfirmationTokenChain();
$chain->pushToken($this->getTokenRequiringReloadIfError());
$this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required');
}
public function testSuppressTokens()
{
$mockToken = $this->getTokenRequiringReload(true, ['suppress']);
$mockToken->expects($this->once())
->method('suppress');
$secondMockToken = $this->getTokenRequiringReloadIfError(true, ['suppress']);
$secondMockToken->expects($this->once())
->method('suppress');
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockToken);
$chain->pushToken($secondMockToken);
$chain->suppressTokens();
}
public function testReloadRequired()
{
$mockToken = $this->getTokenRequiringReload(true);
$secondMockToken = $this->getTokenRequiringReload(false);
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockToken);
$chain->pushToken($secondMockToken);
$this->assertTrue($chain->reloadRequired());
}
public function testReloadRequiredIfError()
{
$mockToken = $this->getTokenRequiringReloadIfError(true);
$secondMockToken = $this->getTokenRequiringReloadIfError(false);
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockToken);
$chain->pushToken($secondMockToken);
$this->assertTrue($chain->reloadRequiredIfError());
}
public function testParams()
{
$mockToken = $this->getTokenRequiringReload(true, ['params']);
$mockToken->expects($this->once())
->method('params')
->with($this->isTrue())
->will($this->returnValue(['mockTokenParam' => '1']));
$secondMockToken = $this->getTokenRequiringReload(true, ['params']);
$secondMockToken->expects($this->once())
->method('params')
->with($this->isTrue())
->will($this->returnValue(['secondMockTokenParam' => '2']));
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockToken);
$chain->pushToken($secondMockToken);
$this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->params(true));
$mockToken = $this->getTokenRequiringReload(true, ['params']);
$mockToken->expects($this->once())
->method('params')
->with($this->isFalse())
->will($this->returnValue(['mockTokenParam' => '1']));
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockToken);
$this->assertEquals(['mockTokenParam' => '1'], $chain->params(false));
}
public function testGetRedirectUrlBase()
{
$mockUrlToken = $this->createPartialMock(URLConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']);
$mockUrlToken->expects($this->any())
->method('reloadRequired')
->will($this->returnValue(true));
$mockUrlToken->expects($this->any())
->method('getRedirectUrlBase')
->will($this->returnValue('url-base'));
$mockParameterToken = $this->createPartialMock(ParameterConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']);
$mockParameterToken->expects($this->any())
->method('reloadRequired')
->will($this->returnValue(true));
$mockParameterToken->expects($this->any())
->method('getRedirectUrlBase')
->will($this->returnValue('parameter-base'));
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockParameterToken);
$chain->pushToken($mockUrlToken);
$this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority');
// Push them in reverse order to check priority still correct
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockUrlToken);
$chain->pushToken($mockParameterToken);
$this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority');
}
public function testGetRedirectUrlParams()
{
$mockToken = $this->getTokenRequiringReload(true, ['params']);
$mockToken->expects($this->once())
->method('params')
->will($this->returnValue(['mockTokenParam' => '1']));
$secondMockToken = $this->getTokenRequiringReload(true, ['params']);
$secondMockToken->expects($this->once())
->method('params')
->will($this->returnValue(['secondMockTokenParam' => '2']));
$chain = new ConfirmationTokenChain();
$chain->pushToken($mockToken);
$chain->pushToken($secondMockToken);
$params = $chain->getRedirectUrlParams();
$this->assertEquals('1', $params['mockTokenParam']);
$this->assertEquals('2', $params['secondMockTokenParam']);
}
}

View File

@ -48,7 +48,7 @@ class ErrorControlChainMiddlewareTest extends SapphireTest
$this->assertInstanceOf(HTTPResponse::class, $result);
$location = $result->getHeader('Location');
$this->assertContains('?flush=1&flushtoken=', $location);
$this->assertContains('flush=1&flushtoken=', $location);
$this->assertNotContains('Security/login', $location);
}
@ -73,4 +73,104 @@ class ErrorControlChainMiddlewareTest extends SapphireTest
$this->assertNotContains('?flush=1&flushtoken=', $location);
$this->assertContains('Security/login', $location);
}
public function testLiveBuildAdmin()
{
// Mock admin
$adminID = $this->logInWithPermission('ADMIN');
$this->logOut();
// Mock app
$app = new HTTPApplication(new BlankKernel(BASE_PATH));
$app->getKernel()->setEnvironment(Kernel::LIVE);
// Test being logged in as admin
$chain = new ErrorControlChainMiddleware($app);
$request = new HTTPRequest('GET', '/dev/build/');
$request->setSession(new Session(['loggedInAs' => $adminID]));
$result = $chain->process($request, function () {
return null;
});
$this->assertInstanceOf(HTTPResponse::class, $result);
$location = $result->getHeader('Location');
$this->assertContains('/dev/build', $location);
$this->assertContains('devbuildtoken=', $location);
$this->assertNotContains('Security/login', $location);
}
public function testLiveBuildUnauthenticated()
{
// Mock app
$app = new HTTPApplication(new BlankKernel(BASE_PATH));
$app->getKernel()->setEnvironment(Kernel::LIVE);
// Test being logged in as no one
Security::setCurrentUser(null);
$chain = new ErrorControlChainMiddleware($app);
$request = new HTTPRequest('GET', '/dev/build');
$request->setSession(new Session(['loggedInAs' => 0]));
$result = $chain->process($request, function () {
return null;
});
// Should be directed to login, not to flush
$this->assertInstanceOf(HTTPResponse::class, $result);
$location = $result->getHeader('Location');
$this->assertNotContains('/dev/build', $location);
$this->assertNotContains('?devbuildtoken=', $location);
$this->assertContains('Security/login', $location);
}
public function testLiveBuildAndFlushAdmin()
{
// Mock admin
$adminID = $this->logInWithPermission('ADMIN');
$this->logOut();
// Mock app
$app = new HTTPApplication(new BlankKernel(BASE_PATH));
$app->getKernel()->setEnvironment(Kernel::LIVE);
// Test being logged in as admin
$chain = new ErrorControlChainMiddleware($app);
$request = new HTTPRequest('GET', '/dev/build/', ['flush' => '1']);
$request->setSession(new Session(['loggedInAs' => $adminID]));
$result = $chain->process($request, function () {
return null;
});
$this->assertInstanceOf(HTTPResponse::class, $result);
$location = $result->getHeader('Location');
$this->assertContains('/dev/build', $location);
$this->assertContains('flush=1', $location);
$this->assertContains('devbuildtoken=', $location);
$this->assertContains('flushtoken=', $location);
$this->assertNotContains('Security/login', $location);
}
public function testLiveBuildAndFlushUnauthenticated()
{
// Mock app
$app = new HTTPApplication(new BlankKernel(BASE_PATH));
$app->getKernel()->setEnvironment(Kernel::LIVE);
// Test being logged in as no one
Security::setCurrentUser(null);
$chain = new ErrorControlChainMiddleware($app);
$request = new HTTPRequest('GET', '/dev/build', ['flush' => '1']);
$request->setSession(new Session(['loggedInAs' => 0]));
$result = $chain->process($request, function () {
return null;
});
// Should be directed to login, not to flush
$this->assertInstanceOf(HTTPResponse::class, $result);
$location = $result->getHeader('Location');
$this->assertNotContains('/dev/build', $location);
$this->assertNotContains('flush=1', $location);
$this->assertNotContains('devbuildtoken=', $location);
$this->assertNotContains('flushtoken=', $location);
$this->assertContains('Security/login', $location);
}
}

View File

@ -149,14 +149,14 @@ class ParameterConfirmationTokenTest extends SapphireTest
}
/**
* currentAbsoluteURL needs to handle base or url being missing, or any combination of slashes.
* currentURL needs to handle base or url being missing, or any combination of slashes.
*
* There should always be exactly one slash between each part in the result, and any trailing slash
* should be preserved.
*
* @dataProvider dataProviderURLs
*/
public function testCurrentAbsoluteURLHandlesSlashes($url)
public function testCurrentURLHandlesSlashes($url)
{
$this->request->setUrl($url);

View File

@ -0,0 +1,148 @@
<?php
namespace SilverStripe\Core\Tests\Startup;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Startup\URLConfirmationToken;
use SilverStripe\Core\Tests\Startup\URLConfirmationTokenTest\StubToken;
use SilverStripe\Core\Tests\Startup\URLConfirmationTokenTest\StubValidToken;
use SilverStripe\Dev\SapphireTest;
class URLConfirmationTokenTest extends SapphireTest
{
public function testValidToken()
{
$request = new HTTPRequest('GET', 'token/test/url', ['tokentesturltoken' => 'value']);
$validToken = new StubValidToken('token/test/url', $request);
$this->assertTrue($validToken->urlMatches());
$this->assertFalse($validToken->urlExistsInBackURL());
$this->assertTrue($validToken->tokenProvided()); // Actually forced to true for this test
$this->assertFalse($validToken->reloadRequired());
$this->assertFalse($validToken->reloadRequiredIfError());
$this->assertStringStartsWith(Controller::join_links(BASE_URL, '/', 'token/test/url'), $validToken->redirectURL());
}
public function testTokenWithLeadingSlashInUrl()
{
$request = new HTTPRequest('GET', '/leading/slash/url', []);
$leadingSlash = new StubToken('leading/slash/url', $request);
$this->assertTrue($leadingSlash->urlMatches());
$this->assertFalse($leadingSlash->urlExistsInBackURL());
$this->assertFalse($leadingSlash->tokenProvided());
$this->assertTrue($leadingSlash->reloadRequired());
$this->assertTrue($leadingSlash->reloadRequiredIfError());
$this->assertContains('leading/slash/url', $leadingSlash->redirectURL());
$this->assertContains('leadingslashurltoken', $leadingSlash->redirectURL());
}
public function testTokenWithTrailingSlashInUrl()
{
$request = new HTTPRequest('GET', 'trailing/slash/url/', []);
$trailingSlash = new StubToken('trailing/slash/url', $request);
$this->assertTrue($trailingSlash->urlMatches());
$this->assertFalse($trailingSlash->urlExistsInBackURL());
$this->assertFalse($trailingSlash->tokenProvided());
$this->assertTrue($trailingSlash->reloadRequired());
$this->assertTrue($trailingSlash->reloadRequiredIfError());
$this->assertContains('trailing/slash/url', $trailingSlash->redirectURL());
$this->assertContains('trailingslashurltoken', $trailingSlash->redirectURL());
}
public function testTokenWithUrlMatchedInBackUrl()
{
$request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']);
$backUrl = new StubToken('back/url', $request);
$this->assertFalse($backUrl->urlMatches());
$this->assertTrue($backUrl->urlExistsInBackURL());
$this->assertFalse($backUrl->tokenProvided());
$this->assertFalse($backUrl->reloadRequired());
$this->assertTrue($backUrl->reloadRequiredIfError());
$home = (BASE_URL ?: '/') . '?';
$this->assertStringStartsWith($home, $backUrl->redirectURL());
$this->assertContains('backurltoken', $backUrl->redirectURL());
}
public function testUrlSuppressionWhenTokenMissing()
{
// Check suppression
$request = new HTTPRequest('GET', 'test/url', []);
$token = new StubToken('test/url', $request);
$this->assertEquals('test/url', $request->getURL(false));
$token->suppress();
$this->assertEquals('', $request->getURL(false));
}
public function testPrepareTokens()
{
$request = new HTTPRequest('GET', 'test/url', []);
$token = URLConfirmationToken::prepare_tokens(
[
'test/url',
'test',
'url'
],
$request
);
// Test no invalid tokens
$this->assertEquals('test/url', $token->getURLToCheck());
$this->assertNotEquals('test/url', $request->getURL(false), 'prepare_tokens() did not suppress URL');
}
public function testPrepareTokensDoesntSuppressWhenNotMatched()
{
$request = new HTTPRequest('GET', 'test/url', []);
$token = URLConfirmationToken::prepare_tokens(
['another/url'],
$request
);
$this->assertEmpty($token);
$this->assertEquals('test/url', $request->getURL(false), 'prepare_tokens() incorrectly suppressed URL');
}
public function testPrepareTokensWithUrlMatchedInBackUrl()
{
// Test backurl token
$request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']);
$token = URLConfirmationToken::prepare_tokens(
[ 'back/url' ],
$request
);
$this->assertNotEmpty($token);
$this->assertEquals('back/url', $token->getURLToCheck());
$this->assertNotEquals('back/url', $request->getURL(false), 'prepare_tokens() did not suppress URL');
}
public function dataProviderURLs()
{
return [
[''],
['/'],
['bar'],
['bar/'],
['/bar'],
['/bar/'],
];
}
/**
* currentURL needs to handle base or url being missing, or any combination of slashes.
*
* There should always be exactly one slash between each part in the result, and any trailing slash
* should be preserved.
*
* @dataProvider dataProviderURLs
*/
public function testCurrentURLHandlesSlashes($url)
{
$request = new HTTPRequest('GET', $url, []);
$token = new StubToken(
'another/url',
$request
);
$expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/';
$this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url");
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace SilverStripe\Core\Tests\Startup\URLConfirmationTokenTest;
use SilverStripe\Core\Startup\URLConfirmationToken;
use SilverStripe\Dev\TestOnly;
/**
* Dummy url token
*/
class StubToken extends URLConfirmationToken implements TestOnly
{
public function urlMatches()
{
return parent::urlMatches();
}
public function currentURL()
{
return parent::currentURL();
}
public function redirectURL()
{
return parent::redirectURL();
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace SilverStripe\Core\Tests\Startup\URLConfirmationTokenTest;
/**
* A token that always validates a given token
*/
class StubValidToken extends StubToken
{
protected function checkToken($token)
{
return true;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\CheckboxField_Readonly;
class CheckboxFieldReadonlyTest extends SapphireTest
{
public function testPerformReadonlyTransformation()
{
$field = new CheckboxField_Readonly('Test');
$result = $field->performReadonlyTransformation();
$this->assertInstanceOf(CheckboxField_Readonly::class, $result);
$this->assertNotSame($result, $field);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace SilverStripe\Forms\Tests\CheckboxSetFieldTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class MultiEnumArticle extends DataObject implements TestOnly
{
private static $table_name = 'CheckboxSetFieldTest_MultiEnumArticle';
private static $db = array(
"Content" => "Text",
"Colours" => "MultiEnum('Red,Blue,Green')",
);
}

View File

@ -0,0 +1,98 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Forms\Tests\CheckboxSetFieldTest\MultiEnumArticle;
use SilverStripe\ORM\DB;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\Connect\MySQLDatabase;
class CheckboxSetFieldMulitEnumTest extends SapphireTest
{
protected $usesDatabase = true;
public static function getExtraDataObjects()
{
// Don't add this for other database
if (DB::get_conn() instanceof MySQLDatabase) {
return [
MultiEnumArticle::class,
];
}
}
public function setUp()
{
if (!(DB::get_conn() instanceof MySQLDatabase)) {
$this->markTestSkipped('DBMultiEnum only supported by MySQL');
return;
}
parent::setUp();
}
public function tearDown()
{
if (!(DB::get_conn() instanceof MySQLDatabase)) {
return;
}
parent::tearDown();
}
public function testLoadDataFromMultiEnum()
{
$article = new MultiEnumArticle();
$article->Colours = 'Red,Green';
$field = new CheckboxSetField(
'Colours',
'Colours',
[
'Red' => 'Red',
'Blue' => 'Blue',
'Green' => 'Green',
]
);
$form = new Form(
Controller::curr(),
'Form',
new FieldList($field),
new FieldList()
);
$form->loadDataFrom($article);
$value = $field->Value();
$this->assertEquals(['Red', 'Green'], $value);
}
public function testSavingIntoMultiEnum()
{
$field = new CheckboxSetField(
'Colours',
'Colours',
[
'Red' => 'Red',
'Blue' => 'Blue',
'Green' => 'Green',
]
);
$article = new MultiEnumArticle();
$field->setValue(array('Red' => 'Red', 'Blue' => 'Blue'));
$field->saveInto($article);
$article->write();
$dbValue = DB::query(
sprintf(
'SELECT "Colours" FROM "CheckboxSetFieldTest_MultiEnumArticle" WHERE "ID" = %s',
$article->ID
)
)->value();
// JSON encoded values
$this->assertEquals('Red,Blue', $dbValue);
}
}

View File

@ -2,13 +2,14 @@
namespace SilverStripe\Forms\Tests;
use PHPUnit_Framework_Error;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\TextField;
class CompositeFieldTest extends SapphireTest
{
@ -36,6 +37,9 @@ class CompositeFieldTest extends SapphireTest
$this->assertEquals(0, $compositeOuter->fieldPosition('A'));
$this->assertEquals(1, $compositeOuter->fieldPosition('AB'));
$this->assertEquals(2, $compositeOuter->fieldPosition('B'));
$this->assertFalse($compositeOuter->fieldPosition(null), 'Falsy input should return false');
$this->assertFalse($compositeOuter->fieldPosition('FOO'), 'Non-exitent child should return false');
}
public function testTag()
@ -124,4 +128,146 @@ class CompositeFieldTest extends SapphireTest
$this->assertEquals($expectedChildren, $field->getChildren());
$this->assertEquals($field, $expectedChildren->getContainerField());
}
public function testExtraClass()
{
$field = CompositeField::create();
$field->setColumnCount(3);
$result = $field->extraClass();
$this->assertContains('field', $result, 'Default class was not added');
$this->assertContains('CompositeField', $result, 'Default class was not added');
$this->assertContains('multicolumn', $result, 'Multi column field did not have extra class added');
}
public function testGetAttributes()
{
$field = CompositeField::create();
$field->setLegend('test');
$result = $field->getAttributes();
$this->assertNull($result['tabindex']);
$this->assertNull($result['type']);
$this->assertNull($result['value']);
$this->assertSame('test', $result['title']);
}
public function testGetAttributesReturnsEmptyTitleForFieldSets()
{
$field = CompositeField::create();
$field->setLegend('not used');
$field->setTag('fieldset');
$result = $field->getAttributes();
$this->assertNull($result['title']);
}
/**
* @expectedException PHPUnit_Framework_Error
* @expectedExceptionMessageRegExp /a field called 'Test' appears twice in your form.*TextField.*TextField/
*/
public function testCollateDataFieldsThrowsErrorOnDuplicateChildren()
{
$field = CompositeField::create(
TextField::create('Test'),
TextField::create('Test')
);
$list = [];
$field->collateDataFields($list);
}
public function testCollateDataFieldsWithSaveableOnly()
{
$field = CompositeField::create(
TextField::create('Test')
->setReadonly(false)
->setDisabled(true)
);
$list = [];
$field->collateDataFields($list, true);
$this->assertEmpty($list, 'Unsaveable fields should not be collated when $saveableOnly = true');
$field->collateDataFields($list, false);
$this->assertNotEmpty($list, 'Unsavable fields should be collated when $saveableOnly = false');
}
public function testSetDisabledPropagatesToChildren()
{
$field = CompositeField::create(
$testField = TextField::create('Test')
->setDisabled(false)
)->setDisabled(true);
$this->assertTrue($field->fieldByName('Test')->isDisabled(), 'Children should also be set to disabled');
}
public function testIsComposite()
{
$this->assertTrue(CompositeField::create()->isComposite());
}
public function testMakeFieldReadonlyPassedFieldName()
{
$field = CompositeField::create(
TextField::create('Test')->setDisabled(false)
);
$this->assertFalse($field->fieldByName('Test')->isReadonly());
$this->assertTrue($field->makeFieldReadonly('Test'), 'makeFieldReadonly should return true');
$this->assertTrue($field->fieldByName('Test')->isReadonly(), 'Named child field should be made readonly');
}
public function testMakeFieldReadonlyPassedFormField()
{
$field = CompositeField::create(
$testField = TextField::create('Test')->setDisabled(false)
);
$this->assertFalse($field->fieldByName('Test')->isReadonly());
$this->assertTrue($field->makeFieldReadonly($testField), 'makeFieldReadonly should return true');
$this->assertTrue($field->fieldByName('Test')->isReadonly(), 'Named child field should be made readonly');
}
public function testMakeFieldReadonlyWithNestedCompositeFields()
{
$field = CompositeField::create(
CompositeField::create(
TextField::create('Test')->setDisabled(false)
)
);
$this->assertFalse($field->getChildren()->first()->fieldByName('Test')->isReadonly());
$this->assertTrue($field->makeFieldReadonly('Test'), 'makeFieldReadonly should return true');
$this->assertTrue(
$field->getChildren()->first()->fieldByName('Test')->isReadonly(),
'Named child field should be made readonly'
);
}
public function testMakeFieldReadonlyReturnsFalseWhenFieldNotFound()
{
$field = CompositeField::create(
CompositeField::create(
TextField::create('Test')
)
);
$this->assertFalse(
$field->makeFieldReadonly('NonExistent'),
'makeFieldReadonly should return false when field is not found'
);
}
public function testDebug()
{
$field = new CompositeField(
new TextField('TestTextField')
);
$field->setName('TestComposite');
$result = $field->debug();
$this->assertContains(CompositeField::class . ' (TestComposite)', $result);
$this->assertContains('TestTextField', $result);
$this->assertContains('<ul', $result, 'Result should be formatted as a <ul>');
}
}

View File

@ -7,11 +7,23 @@ use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\ConfirmedPasswordField;
use SilverStripe\Forms\FieldList;
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)
->setTestNames([]);
}
public function testSetValue()
{
@ -25,6 +37,9 @@ class ConfirmedPasswordFieldTest extends SapphireTest
$this->assertEquals('valueB', $field->children->fieldByName($field->getName() . '[_ConfirmPassword]')->Value());
}
/**
* @useDatabase true
*/
public function testHashHidden()
{
$field = new ConfirmedPasswordField('Password', 'Password', 'valueA');
@ -83,33 +98,40 @@ class ConfirmedPasswordFieldTest extends SapphireTest
$field = new ConfirmedPasswordField(
'Test',
'Testing',
array(
"_Password" => "abc123",
"_ConfirmPassword" => "abc123"
)
[
'_Password' => 'abc123',
'_ConfirmPassword' => 'abc123',
]
);
$validator = new RequiredFields();
/** @skipUpgrade */
new Form(Controller::curr(), 'Form', new FieldList($field), new FieldList(), $validator);
$this->assertTrue(
$field->validate($validator),
"Validates when both passwords are the same"
'Validates when both passwords are the same'
);
$field->setName("TestNew"); //try changing name of field
$field->setName('TestNew'); //try changing name of field
$this->assertTrue(
$field->validate($validator),
"Validates when field name is changed"
'Validates when field name is changed'
);
//non-matching password should make the field invalid
$field->setValue(
array(
"_Password" => "abc123",
"_ConfirmPassword" => "123abc"
)
);
$field->setValue([
'_Password' => 'abc123',
'_ConfirmPassword' => '123abc',
]);
$this->assertFalse(
$field->validate($validator),
"Does not validate when passwords differ"
'Does not validate when passwords differ'
);
// Empty passwords should make the field invalid
$field->setCanBeEmpty(false);
$field->setValue([
'_Password' => '',
'_ConfirmPassword' => '',
]);
$this->assertFalse(
$field->validate($validator),
'Empty passwords should not be allowed when canBeEmpty is false'
);
}
@ -123,17 +145,220 @@ class ConfirmedPasswordFieldTest extends SapphireTest
new FieldList()
);
$form->loadDataFrom(
array(
'Password' => array(
$form->loadDataFrom([
'Password' => [
'_Password' => '123',
'_ConfirmPassword' => '999',
)
)
);
],
]);
$this->assertEquals('123', $field->children->first()->Value());
$this->assertEquals('999', $field->children->last()->Value());
$this->assertNotEquals($field->children->first()->Value(), $field->children->last()->Value());
}
/**
* @param int|null $minLength
* @param int|null $maxLength
* @param bool $expectValid
* @param string $expectedMessage
* @dataProvider lengthValidationProvider
*/
public function testLengthValidation($minLength, $maxLength, $expectValid, $expectedMessage = '')
{
$field = new ConfirmedPasswordField('Test', 'Testing', [
'_Password' => 'abc123',
'_ConfirmPassword' => 'abc123',
]);
$field->setMinLength($minLength)->setMaxLength($maxLength);
$validator = new RequiredFields();
$result = $field->validate($validator);
$this->assertSame($expectValid, $result, 'Validate method should return its result');
$this->assertSame($expectValid, $validator->getResult()->isValid());
if ($expectedMessage) {
$this->assertContains($expectedMessage, $validator->getResult()->serialize());
}
}
/**
* @return array[]
*/
public function lengthValidationProvider()
{
return [
'valid: within min and max' => [3, 8, true],
'invalid: lower than min with max' => [8, 12, false, 'Passwords must be 8 to 12 characters long'],
'valid: greater than min' => [3, null, true],
'invalid: lower than min' => [8, null, false, 'Passwords must be at least 8 characters long'],
'valid: less than max' => [null, 8, true],
'invalid: greater than max' => [null, 4, false, 'Passwords must be at most 4 characters long'],
];
}
public function testStrengthValidation()
{
$field = new ConfirmedPasswordField('Test', 'Testing', [
'_Password' => 'abc',
'_ConfirmPassword' => 'abc',
]);
$field->setRequireStrongPassword(true);
$validator = new RequiredFields();
$result = $field->validate($validator);
$this->assertFalse($result, 'Validate method should return its result');
$this->assertFalse($validator->getResult()->isValid());
$this->assertContains(
'Passwords must have at least one digit and one alphanumeric character',
$validator->getResult()->serialize()
);
}
public function testCurrentPasswordValidation()
{
$field = new ConfirmedPasswordField('Test', 'Testing', [
'_Password' => 'abc',
'_ConfirmPassword' => 'abc',
]);
$field->setRequireExistingPassword(true);
$validator = new RequiredFields();
$result = $field->validate($validator);
$this->assertFalse($result, 'Validate method should return its result');
$this->assertFalse($validator->getResult()->isValid());
$this->assertContains(
'You must enter your current password',
$validator->getResult()->serialize()
);
}
public function testMustBeLoggedInToChangePassword()
{
$field = new ConfirmedPasswordField('Test', 'Testing');
$field->setRequireExistingPassword(true);
$field->setValue([
'_CurrentPassword' => 'foo',
'_Password' => 'abc',
'_ConfirmPassword' => 'abc',
]);
$validator = new RequiredFields();
$this->logOut();
$result = $field->validate($validator);
$this->assertFalse($result, 'Validate method should return its result');
$this->assertFalse($validator->getResult()->isValid());
$this->assertContains(
'You must be logged in to change your password',
$validator->getResult()->serialize()
);
}
/**
* @useDatabase true
*/
public function testValidateCorrectPassword()
{
$this->logInWithPermission('ADMIN');
$field = new ConfirmedPasswordField('Test', 'Testing');
$field->setRequireExistingPassword(true);
$field->setValue([
'_CurrentPassword' => 'foo-not-going-to-be-the-correct-password',
'_Password' => 'abc',
'_ConfirmPassword' => 'abc',
]);
$validator = new RequiredFields();
$result = $field->validate($validator);
$this->assertFalse($result, 'Validate method should return its result');
$this->assertFalse($validator->getResult()->isValid());
$this->assertContains(
'The current password you have entered is not correct',
$validator->getResult()->serialize()
);
}
public function testTitle()
{
$this->assertNull(ConfirmedPasswordField::create('Test')->Title(), 'Should not have it\'s own title');
}
public function testSetTitlePropagatesToPasswordField()
{
/** @var ConfirmedPasswordField $field */
$field = ConfirmedPasswordField::create('Test')
->setTitle('My password');
$this->assertSame('My password', $field->getPasswordField()->Title());
}
public function testSetRightTitlePropagatesToChildren()
{
$field = new ConfirmedPasswordField('Test');
$this->assertCount(2, $field->getChildren());
foreach ($field->getChildren() as $child) {
$this->assertEmpty($child->RightTitle());
}
$field->setRightTitle('Please confirm');
foreach ($field->getChildren() as $child) {
$this->assertSame('Please confirm', $child->RightTitle());
}
}
public function testSetChildrenTitles()
{
$field = new ConfirmedPasswordField('Test');
$field->setRequireExistingPassword(true);
$field->setChildrenTitles([
'Current Password',
'Password',
'Confirm Password',
]);
$this->assertSame('Current Password', $field->getChildren()->shift()->Title());
$this->assertSame('Password', $field->getChildren()->shift()->Title());
$this->assertSame('Confirm Password', $field->getChildren()->shift()->Title());
}
public function testPerformReadonlyTransformation()
{
$field = new ConfirmedPasswordField('Test', 'Change it');
$result = $field->performReadonlyTransformation();
$this->assertInstanceOf(ReadonlyField::class, $result);
$this->assertSame('Change it', $result->Title());
$this->assertContains('***', $result->Value());
}
public function testPerformDisabledTransformation()
{
$field = new ConfirmedPasswordField('Test', 'Change it');
$result = $field->performDisabledTransformation();
$this->assertInstanceOf(ReadonlyField::class, $result);
}
public function testSetRequireExistingPasswordOnlyRunsOnce()
{
$field = new ConfirmedPasswordField('Test', 'Change it');
$this->assertCount(2, $field->getChildren());
$field->setRequireExistingPassword(true);
$this->assertCount(3, $field->getChildren(), 'Current password field was not pushed');
$field->setRequireExistingPassword(true);
$this->assertCount(3, $field->getChildren(), 'Current password field should not be pushed again');
$field->setRequireExistingPassword(false);
$this->assertCount(2, $field->getChildren(), 'Current password field should not be removed');
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\CurrencyField_Disabled;
use SilverStripe\ORM\FieldType\DBCurrency;
class CurrencyFieldDisabledTest extends SapphireTest
{
public function testFieldWithValue()
{
$field = new CurrencyField_Disabled('Test', '', '$5.00');
$result = $field->Field();
$this->assertContains('<input', $result, 'An input should be rendered');
$this->assertContains('disabled', $result, 'The input should be disabled');
$this->assertContains('$5.00', $result, 'The value should be rendered');
}
/**
* @todo: Update the expectation when intl for currencies is implemented
*/
public function testFieldWithCustomisedCurrencySymbol()
{
DBCurrency::config()->update('currency_symbol', '€');
$field = new CurrencyField_Disabled('Test', '', '€5.00');
$result = $field->Field();
$this->assertContains('<input', $result, 'An input should be rendered');
$this->assertContains('disabled', $result, 'The input should be disabled');
$this->assertContains('€5.00', $result, 'The value should be rendered');
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\CurrencyField_Readonly;
use SilverStripe\ORM\FieldType\DBCurrency;
class CurrencyFieldReadonlyTest extends SapphireTest
{
public function testPerformReadonlyTransformation()
{
$field = new CurrencyField_Readonly('Test', '', '$5.00');
$result = $field->performReadonlyTransformation();
$this->assertInstanceOf(CurrencyField_Readonly::class, $result);
$this->assertNotSame($result, $field, 'Should return a clone of the field');
}
public function testFieldWithValue()
{
$field = new CurrencyField_Readonly('Test', '', '$5.00');
$result = $field->Field();
$this->assertContains('<input', $result, 'An input should be rendered');
$this->assertContains('readonly', $result, 'The input should be readonly');
$this->assertContains('$5.00', $result, 'The value should be rendered');
}
public function testFieldWithOutValue()
{
DBCurrency::config()->update('currency_symbol', 'AUD');
$field = new CurrencyField_Readonly('Test', '', null);
$result = $field->Field();
$this->assertContains('<input', $result, 'An input should be rendered');
$this->assertContains('readonly', $result, 'The input should be readonly');
$this->assertContains('AUD0.00', $result, 'The value should be rendered');
}
/**
* @todo: Update the expectation when intl for currencies is implemented
*/
public function testFieldWithCustomisedCurrencySymbol()
{
DBCurrency::config()->update('currency_symbol', '€');
$field = new CurrencyField_Readonly('Test', '', '€5.00');
$result = $field->Field();
$this->assertContains('<input', $result, 'An input should be rendered');
$this->assertContains('readonly', $result, 'The input should be readonly');
$this->assertContains('€5.00', $result, 'The value should be rendered');
}
}

View File

@ -5,6 +5,7 @@ namespace SilverStripe\Forms\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\CurrencyField;
use SilverStripe\Forms\CurrencyField_Readonly;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\ORM\FieldType\DBCurrency;
@ -124,56 +125,56 @@ class CurrencyFieldTest extends SapphireTest
//tests with default currency symbol setting
$f->setValue('123.45');
$this->assertEquals(
$f->value,
$f->Value(),
'$123.45',
'Prepends dollar sign to positive decimal'
);
$f->setValue('-123.45');
$this->assertEquals(
$f->value,
$f->Value(),
'$-123.45',
'Prepends dollar sign to negative decimal'
);
$f->setValue('$1');
$this->assertEquals(
$f->value,
$f->Value(),
'$1.00',
'Formats small value'
);
$f->setValue('$2.5');
$this->assertEquals(
$f->value,
$f->Value(),
'$2.50',
'Formats small value'
);
$f->setValue('$2500000.13');
$this->assertEquals(
$f->value,
$f->Value(),
'$2,500,000.13',
'Formats large value'
);
$f->setValue('$2.50000013');
$this->assertEquals(
$f->value,
$f->Value(),
'$2.50',
'Truncates long decimal portions'
);
$f->setValue('test123.00test');
$this->assertEquals(
$f->value,
$f->Value(),
'$123.00',
'Strips alpha values'
);
$f->setValue('test');
$this->assertEquals(
$f->value,
$f->Value(),
'$0.00',
'Does not set alpha values'
);
@ -183,56 +184,56 @@ class CurrencyFieldTest extends SapphireTest
$f->setValue('123.45');
$this->assertEquals(
$f->value,
$f->Value(),
'€123.45',
'Prepends dollar sign to positive decimal'
);
$f->setValue('-123.45');
$this->assertEquals(
$f->value,
$f->Value(),
'€-123.45',
'Prepends dollar sign to negative decimal'
);
$f->setValue('€1');
$this->assertEquals(
$f->value,
$f->Value(),
'€1.00',
'Formats small value'
);
$f->setValue('€2.5');
$this->assertEquals(
$f->value,
$f->Value(),
'€2.50',
'Formats small value'
);
$f->setValue('€2500000.13');
$this->assertEquals(
$f->value,
$f->Value(),
'€2,500,000.13',
'Formats large value'
);
$f->setValue('€2.50000013');
$this->assertEquals(
$f->value,
$f->Value(),
'€2.50',
'Truncates long decimal portions'
);
$f->setValue('test123.00test');
$this->assertEquals(
$f->value,
$f->Value(),
'€123.00',
'Strips alpha values'
);
$f->setValue('test');
$this->assertEquals(
$f->value,
$f->Value(),
'€0.00',
'Does not set alpha values'
);
@ -282,4 +283,30 @@ class CurrencyFieldTest extends SapphireTest
-123.45
);
}
public function testDataValueReturnsEmptyFloat()
{
$field = new CurrencyField('Test', '', null);
$this->assertSame(0.00, $field->dataValue());
}
public function testPerformReadonlyTransformation()
{
$field = new CurrencyField('Test');
$result = $field->performReadonlyTransformation();
$this->assertInstanceOf(CurrencyField_Readonly::class, $result);
}
public function testInvalidCurrencySymbol()
{
$field = new CurrencyField('Test', '', '$5.00');
$validator = new RequiredFields();
DBCurrency::config()->update('currency_symbol', '€');
$result = $field->validate($validator);
$this->assertFalse($result, 'Validation should fail since wrong currency was used');
$this->assertFalse($validator->getResult()->isValid(), 'Validator should receive failed state');
$this->assertContains('Please enter a valid currency', $validator->getResult()->serialize());
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace SilverStripe\Forms;
use PHPUnit_Framework_MockObject_MockObject;
use SilverStripe\Dev\SapphireTest;
class DatalessFieldTest extends SapphireTest
{
public function testGetAttributes()
{
$field = new DatalessField('Name');
$result = $field->getAttributes();
$this->assertSame('hidden', $result['type']);
}
public function testFieldHolderAndSmallFieldHolderReturnField()
{
/** @var DatalessField|PHPUnit_Framework_MockObject_MockObject $mock */
$mock = $this->getMockBuilder(DatalessField::class)
->disableOriginalConstructor()
->setMethods(['Field'])
->getMock();
$properties = [
'foo' => 'bar',
];
$mock->expects($this->exactly(2))->method('Field')->with($properties)->willReturn('boo!');
$fieldHolder = $mock->FieldHolder($properties);
$smallFieldHolder = $mock->SmallFieldHolder($properties);
$this->assertSame('boo!', $fieldHolder);
$this->assertSame('boo!', $smallFieldHolder);
}
public function testPerformReadonlyTransformation()
{
$field = new DatalessField('Test');
$result = $field->performReadonlyTransformation();
$this->assertInstanceOf(DatalessField::class, $result);
$this->assertTrue($result->isReadonly());
}
}

View File

@ -2,17 +2,15 @@
namespace SilverStripe\Forms\Tests;
use IntlDateFormatter;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DateField_Disabled;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBDatetime;
/**
* @skipUpgrade
*/
class DateField_DisabledTest extends SapphireTest
class DateFieldDisabledTest extends SapphireTest
{
protected function setUp()
{
@ -76,4 +74,12 @@ class DateField_DisabledTest extends SapphireTest
$actual = DateField_Disabled::create('Test')->setValue('This is not a date')->Field();
$this->assertEquals($expected, $actual);
}
public function testType()
{
$field = new DateField_Disabled('Test');
$result = $field->Type();
$this->assertContains('readonly', $result, 'Disabled field should be treated as readonly');
$this->assertContains('date_disabled', $result, 'Field should contain date_disabled class');
}
}

View File

@ -5,8 +5,10 @@ namespace SilverStripe\Forms\Tests;
use IntlDateFormatter;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\DateField_Disabled;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBDate;
use SilverStripe\ORM\FieldType\DBDatetime;
/**
@ -225,4 +227,54 @@ class DateFieldTest extends SapphireTest
$dateField->setLocale('de_DE');
$dateField->Value();
}
public function testGetDateFormatHTML5()
{
$field = new DateField('Date');
$field->setHTML5(true);
$this->assertSame(DBDate::ISO_DATE, $field->getDateFormat());
}
public function testGetDateFormatViaSetter()
{
$field = new DateField('Date');
$field->setHTML5(false);
$field->setDateFormat('d-m-Y');
$this->assertSame('d-m-Y', $field->getDateFormat());
}
public function testGetAttributes()
{
$field = new DateField('Date');
$field
->setHTML5(true)
->setMinDate('1980-05-10')
->setMaxDate('1980-05-20');
$result = $field->getAttributes();
$this->assertSame('1980-05-10', $result['min']);
$this->assertSame('1980-05-20', $result['max']);
}
public function testSetSubmittedValueNull()
{
$field = new DateField('Date');
$field->setSubmittedValue(false);
$this->assertNull($field->Value());
}
public function testPerformReadonlyTransformation()
{
$field = new DateField('Date');
$result = $field->performReadonlyTransformation();
$this->assertInstanceOf(DateField_Disabled::class, $result);
$this->assertTrue($result->isReadonly());
}
public function testValidateWithoutValueReturnsTrue()
{
$field = new DateField('Date');
$validator = new RequiredFields();
$this->assertTrue($field->validate($validator));
}
}

View File

@ -86,6 +86,13 @@ class DatetimeFieldTest extends SapphireTest
$this->assertEquals('2003-01-30 11:59:38', $f->dataValue()); // server timezone (Berlin)
}
public function testSetSubmittedValueNull()
{
$field = new DatetimeField('Datetime');
$field->setSubmittedValue(false);
$this->assertNull($field->Value());
}
public function testConstructorWithoutArgs()
{
$f = new DatetimeField('Datetime');
@ -148,21 +155,24 @@ class DatetimeFieldTest extends SapphireTest
public function testValidate()
{
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38');
$this->assertTrue($f->validate(new RequiredFields()));
$field = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38');
$this->assertTrue($field->validate(new RequiredFields()));
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29T23:59:38');
$this->assertTrue($f->validate(new RequiredFields()), 'Normalised ISO');
$field = new DatetimeField('Datetime', 'Datetime', '2003-03-29T23:59:38');
$this->assertTrue($field->validate(new RequiredFields()), 'Normalised ISO');
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29');
$this->assertFalse($f->validate(new RequiredFields()), 'Leaving out time');
$field = new DatetimeField('Datetime', 'Datetime', '2003-03-29');
$this->assertFalse($field->validate(new RequiredFields()), 'Leaving out time');
$f = (new DatetimeField('Datetime', 'Datetime'))
$field = (new DatetimeField('Datetime', 'Datetime'))
->setSubmittedValue('2003-03-29T00:00');
$this->assertTrue($f->validate(new RequiredFields()), 'Leaving out seconds (like many browsers)');
$this->assertTrue($field->validate(new RequiredFields()), 'Leaving out seconds (like many browsers)');
$f = new DatetimeField('Datetime', 'Datetime', 'wrong');
$this->assertFalse($f->validate(new RequiredFields()));
$field = new DatetimeField('Datetime', 'Datetime', 'wrong');
$this->assertFalse($field->validate(new RequiredFields()));
$field = new DatetimeField('Datetime', 'Datetime', false);
$this->assertTrue($field->validate(new RequiredFields()));
}
public function testSetMinDate()
@ -446,6 +456,51 @@ class DatetimeFieldTest extends SapphireTest
$this->assertEquals($attrs['max'], '2010-01-31T23:00:00'); // frontend timezone
}
public function testAttributesNonHTML5()
{
$field = new DatetimeField('Datetime');
$field->setHTML5(false);
$result = $field->getAttributes();
$this->assertSame('text', $result['type']);
}
public function testFrontendToInternalEdgeCases()
{
$field = new DatetimeField('Datetime');
$this->assertNull($field->frontendToInternal(false));
$this->assertNull($field->frontendToInternal('sdfsdfsfs$%^&*'));
}
public function testInternalToFrontendEdgeCases()
{
$field = new DatetimeField('Datetime');
$this->assertNull($field->internalToFrontend(false));
$this->assertNull($field->internalToFrontend('sdfsdfsfs$%^&*'));
}
public function testPerformReadonlyTransformation()
{
$field = new DatetimeField('Datetime');
$result = $field->performReadonlyTransformation();
$this->assertInstanceOf(DatetimeField::class, $result);
$this->assertNotSame($result, $field, 'Readonly field should be cloned');
$this->assertTrue($result->isReadonly());
}
/**
* @expectedException \BadMethodCallException
* @expectedExceptionMessage Can't change timezone after setting a value
*/
public function testSetTimezoneThrowsExceptionWhenChangingTimezoneAfterSettingValue()
{
date_default_timezone_set('Europe/Berlin');
$field = new DatetimeField('Datetime', 'Time', '2003-03-29 23:59:38');
$field->setTimezone('Pacific/Auckland');
}
protected function getMockForm()
{
/** @skipUpgrade */

View File

@ -0,0 +1,38 @@
<?php
namespace SilverStripe\Forms\Tests;
use InvalidArgumentException;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DefaultFormFactory;
use SilverStripe\ORM\DataObject;
class DefaultFormFactoryTest extends SapphireTest
{
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessageRegExp /Missing required context/
*/
public function testGetFormThrowsExceptionOnMissingContext()
{
$factory = new DefaultFormFactory();
$factory->getForm();
}
public function testGetForm()
{
$record = new DataObject();
$record->Title = 'Test';
$factory = new DefaultFormFactory();
$form = $factory->getForm(null, null, ['Record' => $record]);
$this->assertSame($record, $form->getRecord());
}
public function testGetRequiredContext()
{
$factory = new DefaultFormFactory();
$this->assertContains('Record', $factory->getRequiredContext());
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DisabledTransformation;
use SilverStripe\Forms\TextField;
class DisabledTransformationTest extends SapphireTest
{
public function testTransform()
{
$field = new TextField('Test');
$transformation = new DisabledTransformation();
$newField = $transformation->transform($field);
$this->assertTrue($newField->isDisabled(), 'Transformation failed to transform field to be disabled');
}
}

View File

@ -482,29 +482,29 @@ class FieldListTest extends SapphireTest
);
$this->assertEquals(
$tabSetWithTitle->Title(),
'My TabSet Title',
'Automatic conversion of tab identifiers through findOrMakeTab() with FormField::name_to_label()'
$tabSetWithTitle->Title(),
'Existing field title should be used'
);
$tabWithoutTitle = $set->findOrMakeTab('Root.TabWithoutTitle');
$this->assertEquals(
'Tab without title',
$tabWithoutTitle->Title(),
'Tab Without Title',
'Automatic conversion of tab identifiers through findOrMakeTab() with FormField::name_to_label()'
);
$tabWithTitle = $set->findOrMakeTab('Root.TabWithTitle', 'My Tab with Title');
$this->assertEquals(
$tabWithTitle->Title(),
'My Tab with Title',
$tabWithTitle->Title(),
'Setting of simple tab titles through findOrMakeTab()'
);
$childTabWithTitle = $set->findOrMakeTab('Root.TabSetWithoutTitle.NewChildTab', 'My Child Tab Title');
$this->assertEquals(
$childTabWithTitle->Title(),
'My Child Tab Title',
$childTabWithTitle->Title(),
'Setting of nested tab titles through findOrMakeTab() works on last child tab'
);
}

View File

@ -402,4 +402,31 @@ class FormFieldTest extends SapphireTest
$field = new FormField('Test');
$field->Link('bar');
}
/**
* @param string $name
* @param string $expected
* @dataProvider nameToLabelProvider
*/
public function testNameToLabel($name, $expected)
{
$this->assertSame($expected, FormField::name_to_label($name));
}
/**
* @return array[]
*/
public function nameToLabelProvider()
{
return [
['TotalAmount', 'Total amount'],
['Organisation.ZipCode', 'Organisation zip code'],
['Organisation.zipCode', 'Organisation zip code'],
['FooBarBaz', 'Foo bar baz'],
['URLSegment', 'URL segment'],
['ONLYCAPS', 'ONLYCAPS'],
['onlylower', 'Onlylower'],
['SpecialURL', 'Special URL'],
];
}
}

View File

@ -415,7 +415,7 @@ class FormTest extends FunctionalTest
// Firstly, assert that required fields still work when not using an exempt action
$this->assertPartialMatchBySelector(
'#Form_Form_SomeRequiredField_Holder .required',
array('"Some Required Field" is required'),
array('"Some required field" is required'),
'Required fields show a notification on field when left blank'
);
@ -487,7 +487,7 @@ class FormTest extends FunctionalTest
$this->assertPartialMatchBySelector(
'#Form_Form_SomeRequiredField_Holder span.required',
array(
'"Some Required Field" is required'
'"Some required field" is required'
),
'Required fields show a notification on field when left blank'
);

View File

@ -140,7 +140,7 @@ class HTMLEditorConfigTest extends SapphireTest
$this->assertNotContains('plugin1', array_keys($plugins));
$this->assertNotContains('plugin2', array_keys($plugins));
}
public function testRequireJSIncludesAllConfigs()
{
$a = HTMLEditorConfig::get('configA');

View File

@ -9,6 +9,13 @@ use SilverStripe\Forms\HTMLEditor\TinyMCEConfig;
class TinyMCEConfigTest extends SapphireTest
{
public function testEditorIdentifier()
{
$config = TinyMCEConfig::get('myconfig');
$this->assertEquals('myconfig', $config->getOption('editorIdentifier'));
}
/**
* Ensure that all TinyMCEConfig.tinymce_lang are valid
*/

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\PrintableTransformation_TabSet;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
class PrintableTransformationTabSetTest extends SapphireTest
{
public function testFieldHolder()
{
$tabs = [
new Tab('Main'),
new Tab('Secondary'),
$optionsTabSet = new TabSet(
'Options',
'Options',
new Tab('Colours'),
new Tab('Options')
),
];
$transformationTabSet = new PrintableTransformation_TabSet($tabs);
$result = $transformationTabSet->FieldHolder();
$this->assertContains('<h1>Main</h1>', $result);
$this->assertContains('<h1>Secondary</h1>', $result);
$transformationTabSet->setTabSet($optionsTabSet);
$result = $transformationTabSet->FieldHolder();
$this->assertContains('<h2>Options</h2>', $result);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\PrintableTransformation;
use SilverStripe\Forms\PrintableTransformation_TabSet;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
class PrintableTransformationTest extends SapphireTest
{
public function testTransformTabSet()
{
$tab1 = new Tab('Main');
$tab2 = new Tab('Settings');
$tabSet = new TabSet('Root', 'Root', $tab1, $tab2);
$transformation = new PrintableTransformation();
$result = $transformation->transformTabSet($tabSet);
$this->assertInstanceOf(PrintableTransformation_TabSet::class, $result);
$this->assertSame('Root', $result->Title());
}
}

View File

@ -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);

View File

@ -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

View File

@ -4,28 +4,301 @@ namespace SilverStripe\Forms\Tests;
use SilverStripe\Assets\File;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormTemplateHelper;
use SilverStripe\Forms\TreeMultiselectField;
use SilverStripe\ORM\Tests\HierarchyTest\TestObject;
use SilverStripe\View\SSViewer;
class TreeMultiselectFieldTest extends SapphireTest
{
protected static $fixture_file = 'TreeDropdownFieldTest.yml';
public function testReadonly()
protected static $extra_dataobjects = [
TestObject::class,
];
protected $formId = 'TheFormID';
protected $fieldName = 'TestTree';
/**
* Mock object of a generic form
*
* @var Form
*/
protected $form;
/**
* Instance of the TreeMultiselectField
*
* @var TreeMultiselectField
*/
protected $field;
/**
* The File objects of folders loaded from the fixture
*
* @var File[]
*/
protected $folders;
/**
* The array of folder ids
*
* @var int[]
*/
protected $folderIds;
/**
* Concatenated folder ids for use as a value for the field
*
* @var string
*/
protected $fieldValue;
protected function setUp()
{
parent::setUp();
// Don't let other themes interfere with these tests
SSViewer::set_themes([]);
$this->form = $this->buildFormMock();
$this->field = $this->buildField($this->form);
$this->folders = $this->loadFolders();
$this->folderIds = array_map(
static function ($f) {
return $f->ID;
},
$this->folders
);
$this->fieldValue = implode(',', $this->folderIds);
}
/**
* Build a new mock object of a Form
*
* @return Form
*/
protected function buildFormMock()
{
$form = $this->createMock(Form::class);
$form->method('getTemplateHelper')
->willReturn(FormTemplateHelper::singleton());
$form->method('getHTMLID')
->willReturn($this->formId);
return $form;
}
/**
* Build a new instance of TreeMultiselectField
*
* @param Form $form The field form
*
* @return TreeMultiselectField
*/
protected function buildField(Form $form)
{
$field = new TreeMultiselectField($this->fieldName, 'Test tree', File::class);
$field->setForm($form);
return $field;
}
/**
* Load several files from the fixtures and return them in an array
*
* @return File[]
*/
protected function loadFolders()
{
$field = new TreeMultiselectField('TestTree', 'Test tree', File::class);
$asdf = $this->objFromFixture(File::class, 'asdf');
$subfolderfile1 = $this->objFromFixture(File::class, 'subfolderfile1');
$field->setValue(implode(',', [$asdf->ID, $subfolderfile1->ID]));
$readonlyField = $field->performReadonlyTransformation();
$this->assertEquals(
<<<"HTML"
<span id="TestTree_ReadonlyValue" class="readonly">
&lt;Special &amp; characters&gt;, TestFile1InSubfolder
</span><input type="hidden" name="TestTree" value="{$asdf->ID},{$subfolderfile1->ID}" class="hidden" id="TestTree" />
HTML
,
(string)$readonlyField->Field()
return [$asdf, $subfolderfile1];
}
/**
* Test the TreeMultiselectField behaviour with no selected values
*/
public function testEmpty()
{
$field = $this->field;
$fieldId = $field->ID();
$this->assertEquals($fieldId, sprintf('%s_%s', $this->formId, $this->fieldName));
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $fieldId,
'name' => $this->fieldName,
'value' => 'unchanged'
],
$schemaStateDefaults,
true
);
$schemaDataDefaults = $field->getSchemaDataDefaults();
$this->assertArraySubset(
[
'id' => $fieldId,
'name' => $this->fieldName,
'type' => 'text',
'schemaType' => 'SingleSelect',
'component' => 'TreeDropdownField',
'holderId' => sprintf('%s_Holder', $fieldId),
'title' => 'Test tree',
'extraClass' => 'treemultiselect multiple searchable',
'data' => [
'urlTree' => 'field/TestTree/tree',
'showSearch' => true,
'emptyString' => '(Choose File)',
'hasEmptyDefault' => false,
'multiple' => true
]
],
$schemaDataDefaults,
true
);
$items = $field->getItems();
$this->assertCount(0, $items, 'there must be no items selected');
$html = $field->Field();
$this->assertContains($field->ID(), $html);
$this->assertContains('unchanged', $html);
}
/**
* Test the field with some values set
*/
public function testChanged()
{
$field = $this->field;
$field->setValue($this->fieldValue);
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => 'TestTree',
'value' => $this->folderIds
],
$schemaStateDefaults,
true
);
$items = $field->getItems();
$this->assertCount(2, $items, 'there must be exactly 2 items selected');
$html = $field->Field();
$this->assertContains($field->ID(), $html);
$this->assertContains($this->fieldValue, $html);
}
/**
* Test empty field in readonly mode
*/
public function testEmptyReadonly()
{
$field = $this->field->performReadonlyTransformation();
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => 'TestTree',
'value' => 'unchanged'
],
$schemaStateDefaults,
true
);
$schemaDataDefaults = $field->getSchemaDataDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => $this->fieldName,
'type' => 'text',
'schemaType' => 'SingleSelect',
'component' => 'TreeDropdownField',
'holderId' => sprintf('%s_Holder', $field->ID()),
'title' => 'Test tree',
'extraClass' => 'treemultiselectfield_readonly multiple searchable',
'data' => [
'urlTree' => 'field/TestTree/tree',
'showSearch' => true,
'emptyString' => '(Choose File)',
'hasEmptyDefault' => false,
'multiple' => true
]
],
$schemaDataDefaults,
true
);
$items = $field->getItems();
$this->assertCount(0, $items, 'there must be 0 selected items');
$html = $field->Field();
$this->assertContains($field->ID(), $html);
}
/**
* Test changed field in readonly mode
*/
public function testChangedReadonly()
{
$field = $this->field;
$field->setValue($this->fieldValue);
$field = $field->performReadonlyTransformation();
$schemaStateDefaults = $field->getSchemaStateDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => 'TestTree',
'value' => $this->folderIds
],
$schemaStateDefaults,
true
);
$schemaDataDefaults = $field->getSchemaDataDefaults();
$this->assertArraySubset(
[
'id' => $field->ID(),
'name' => $this->fieldName,
'type' => 'text',
'schemaType' => 'SingleSelect',
'component' => 'TreeDropdownField',
'holderId' => sprintf('%s_Holder', $field->ID()),
'title' => 'Test tree',
'extraClass' => 'treemultiselectfield_readonly multiple searchable',
'data' => [
'urlTree' => 'field/TestTree/tree',
'showSearch' => true,
'emptyString' => '(Choose File)',
'hasEmptyDefault' => false,
'multiple' => true
]
],
$schemaDataDefaults,
true
);
$items = $field->getItems();
$this->assertCount(2, $items, 'there must be exactly 2 selected items');
$html = $field->Field();
$this->assertContains($field->ID(), $html);
$this->assertContains($this->fieldValue, $html);
}
}

View File

@ -2,18 +2,65 @@
namespace SilverStripe\Logging\Tests;
use PHPUnit_Framework_MockObject_MockObject;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Logging\DebugViewFriendlyErrorFormatter;
class DebugViewFriendlyErrorFormatterTest extends SapphireTest
{
public function setUp()
protected function setUp()
{
parent::setUp();
Email::config()->set('admin_email', 'testy@mctest.face');
}
public function testFormatPassesRecordCodeToOutput()
{
/** @var DebugViewFriendlyErrorFormatter|PHPUnit_Framework_MockObject_MockObject $mock */
$mock = $this->getMockBuilder(DebugViewFriendlyErrorFormatter::class)
->setMethods(['output'])
->getMock();
$mock->expects($this->once())->method('output')->with(403)->willReturn('foo');
$this->assertSame('foo', $mock->format(['code' => 403]));
}
public function testFormatPassesInstanceStatusCodeToOutputWhenNotProvidedByRecord()
{
/** @var DebugViewFriendlyErrorFormatter|PHPUnit_Framework_MockObject_MockObject $mock */
$mock = $this->getMockBuilder(DebugViewFriendlyErrorFormatter::class)
->setMethods(['output'])
->getMock();
$mock->setStatusCode(404);
$mock->expects($this->once())->method('output')->with(404)->willReturn('foo');
$this->assertSame('foo', $mock->format(['notacode' => 'bar']));
}
public function testFormatBatch()
{
$records = [
['message' => 'bar'],
['open' => 'sausage'],
['horse' => 'caballo'],
];
/** @var DebugViewFriendlyErrorFormatter|PHPUnit_Framework_MockObject_MockObject $mock */
$mock = $this->getMockBuilder(DebugViewFriendlyErrorFormatter::class)
->setMethods(['format'])
->getMock();
$mock->expects($this->exactly(3))
->method('format')
->willReturn('foo');
$this->assertSame('foofoofoo', $mock->formatBatch($records));
}
public function testOutput()
{
$formatter = new DebugViewFriendlyErrorFormatter();
@ -34,4 +81,15 @@ TEXT
$this->assertEquals($expected, $formatter->output(404));
}
public function testOutputReturnsTitleWhenRequestIsAjax()
{
// Mock an AJAX request
Injector::inst()->registerService(new HTTPRequest('GET', '', ['ajax' => true]));
$formatter = new DebugViewFriendlyErrorFormatter();
$formatter->setTitle('The Diary of Anne Frank');
$this->assertSame('The Diary of Anne Frank', $formatter->output(200));
}
}

View File

@ -8,7 +8,7 @@ use SilverStripe\Logging\Tests\DetailedErrorFormatterTest\ErrorGenerator;
class DetailedErrorFormatterTest extends SapphireTest
{
public function testFormat()
public function testFormatWithException()
{
$generator = new ErrorGenerator();
$formatter = new DetailedErrorFormatter();
@ -27,4 +27,48 @@ class DetailedErrorFormatterTest extends SapphireTest
$output
);
}
public function testFormatWithoutException()
{
$record = [
'code' => 401,
'message' => 'Denied',
'file' => 'index.php',
'line' => 4,
];
$formatter = new DetailedErrorFormatter();
$result = $formatter->format($record);
$this->assertContains('ERRNO 401', $result, 'Status code was not found in trace');
$this->assertContains('Denied', $result, 'Message was not found in trace');
$this->assertContains('Line 4 in index.php', $result, 'Line or filename were not found in trace');
$this->assertContains(self::class, $result, 'Backtrace doesn\'t show current test class');
}
public function testFormatBatch()
{
$records = [
[
'code' => 401,
'message' => 'Denied',
'file' => 'index.php',
'line' => 4,
],
[
'code' => 404,
'message' => 'Not found',
'file' => 'admin.php',
'line' => 7,
],
];
$formatter = new DetailedErrorFormatter();
$result = $formatter->formatBatch($records);
$this->assertContains('ERRNO 401', $result, 'First status code was not found in trace');
$this->assertContains('ERRNO 404', $result, 'Second status code was not found in trace');
$this->assertContains('Denied', $result, 'First message was not found in trace');
$this->assertContains('Not found', $result, 'Second message was not found in trace');
}
}

View File

@ -3,7 +3,6 @@
namespace SilverStripe\Logging\Tests;
use Monolog\Handler\HandlerInterface;
use PhpParser\Node\Scalar\MagicConst\Dir;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
@ -13,14 +12,12 @@ use SilverStripe\Logging\HTTPOutputHandler;
class HTTPOutputHandlerTest extends SapphireTest
{
public function setUp()
protected function setUp()
{
parent::setUp();
if (!Director::is_cli()) {
$this->markTestSkipped("This test only runs in CLI mode");
}
if (!Director::isDev()) {
$this->markTestSkipped("This test only runs in dev mode");
$this->markTestSkipped('This test only runs in dev mode');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace SilverStripe\Logging\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Logging\MonologErrorHandler;
class MonologErrorHandlerTest extends SapphireTest
{
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessageRegExp /No Logger property passed to MonologErrorHandler/
*/
public function testStartThrowsExceptionWithoutLoggerDefined()
{
$handler = new MonologErrorHandler();
$handler->start();
}
}

View File

@ -5,9 +5,17 @@ namespace SilverStripe\ORM\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DB;
class DBEnumTest extends SapphireTest
{
protected $extraDataObjects = [
FieldType\DBEnumTestObject::class,
];
protected $usesDatabase = true;
public function testDefault()
{
/** @var DBEnum $enum1 */
@ -28,4 +36,66 @@ class DBEnumTest extends SapphireTest
$this->assertEquals('B', $enum4->getDefaultValue());
$this->assertEquals('B', $enum4->getDefault());
}
public function testObsoleteValues()
{
$obj = new FieldType\DBEnumTestObject();
$colourField = $obj->obj('Colour');
$colourField->setTable('FieldType_DBEnumTestObject');
// Test values prior to any database content
$this->assertEquals(
['Red', 'Blue', 'Green'],
$colourField->getEnumObsolete()
);
// Test values with a record
$obj->Colour = 'Red';
$obj->write();
DBEnum::flushCache();
$this->assertEquals(
['Red', 'Blue', 'Green'],
$colourField->getEnumObsolete()
);
// If the value is removed from the enum, obsolete content is still retained
$colourField->setEnum(['Blue', 'Green', 'Purple']);
DBEnum::flushCache();
$this->assertEquals(
['Blue', 'Green', 'Purple', 'Red'], // Red on the end now, because it's obsolete
$colourField->getEnumObsolete()
);
// Check that old and new data is preserved after a schema update
DB::get_schema()->schemaUpdate(function () use ($colourField) {
$colourField->requireField();
});
$obj2 = new FieldType\DBEnumTestObject();
$obj2->Colour = 'Purple';
$obj2->write();
$this->assertEquals(
['Purple', 'Red'],
FieldType\DBEnumTestObject::get()->sort('Colour')->column('Colour')
);
// Ensure that enum columns are retained
$colourField->setEnum(['Blue', 'Green']);
$this->assertEquals(
['Blue', 'Green', 'Purple', 'Red'],
$colourField->getEnumObsolete()
);
// If obsolete records are deleted, the extra values go away
$obj->delete();
$obj2->delete();
DBEnum::flushCache();
$this->assertEquals(
['Blue', 'Green'],
$colourField->getEnumObsolete()
);
}
}

View File

@ -5,7 +5,7 @@ namespace SilverStripe\ORM\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\Connect\MySQLSchemaManager;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Tests\DataObjectSchemaGenerationTest\SortedObject;
@ -208,7 +208,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest
$schema = DataObject::getSchema();
// Test with blank entries
DBClassName::clear_classname_cache();
DBEnum::flushCache();
$do1 = new TestObject();
$fields = $schema->databaseFields(TestObject::class, false);
$this->assertEquals("DBClassName", $fields['ClassName']);
@ -224,7 +224,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest
// Test with instance of subclass
$item1 = new TestIndexObject();
$item1->write();
DBClassName::clear_classname_cache();
DBEnum::flushCache();
$this->assertEquals(
[
TestObject::class,
@ -237,7 +237,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest
// Test with instance of main class
$item2 = new TestObject();
$item2->write();
DBClassName::clear_classname_cache();
DBEnum::flushCache();
$this->assertEquals(
[
TestObject::class,
@ -252,7 +252,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest
$item1->write();
$item2 = new TestObject();
$item2->write();
DBClassName::clear_classname_cache();
DBEnum::flushCache();
$this->assertEquals(
[
TestObject::class,

View File

@ -8,7 +8,7 @@ class TestIndexObject extends TestObject implements TestOnly
{
private static $table_name = 'DataObjectSchemaGenerationTest_IndexDO';
private static $db = [
'Title' => 'Varchar(255)',
'Title' => 'Varchar(192)',
'Content' => 'Text',
];

View File

@ -66,6 +66,51 @@ class DataObjectTest extends SapphireTest
);
}
/**
* @dataProvider provideSingletons
*/
public function testSingleton($inst, $defaultValue, $altDefaultValue)
{
$inst = $inst();
// Test that populateDefaults() isn't called on singletons
// which can lead to SQL errors during build, and endless loops
if ($defaultValue) {
$this->assertEquals($defaultValue, $inst->MyFieldWithDefault);
} else {
$this->assertEmpty($inst->MyFieldWithDefault);
}
if ($altDefaultValue) {
$this->assertEquals($altDefaultValue, $inst->MyFieldWithAltDefault);
} else {
$this->assertEmpty($inst->MyFieldWithAltDefault);
}
}
public function provideSingletons()
{
// because PHPUnit evalutes test providers *before* setUp methods
// any extensions added in the setUp methods won't be available
// we must return closures to generate the arguments at run time
return array(
'create() static method' => array(function () {
return DataObjectTest\Fixture::create();
}, 'Default Value', 'Default Value'),
'New object creation' => array(function () {
return new DataObjectTest\Fixture();
}, 'Default Value', 'Default Value'),
'singleton() function' => array(function () {
return singleton(DataObjectTest\Fixture::class);
}, null, null),
'singleton() static method' => array(function () {
return DataObjectTest\Fixture::singleton();
}, null, null),
'Manual constructor args' => array(function () {
return new DataObjectTest\Fixture(null, true);
}, null, null),
);
}
public function testDb()
{
$schema = DataObject::getSchema();
@ -765,6 +810,75 @@ class DataObjectTest extends SapphireTest
);
}
public function testChangedFieldsWhenRestoringData()
{
$obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
$obj->FirstName = 'Captain-changed';
$obj->FirstName = 'Captain';
$this->assertEquals(
[],
$obj->getChangedFields(true, DataObject::CHANGE_STRICT)
);
}
public function testChangedFieldsAfterWrite()
{
$obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
$obj->FirstName = 'Captain-changed';
$obj->write();
$obj->FirstName = 'Captain';
$this->assertEquals(
array(
'FirstName' => array(
'before' => 'Captain-changed',
'after' => 'Captain',
'level' => DataObject::CHANGE_VALUE,
),
),
$obj->getChangedFields(true, DataObject::CHANGE_VALUE)
);
}
public function testForceChangeCantBeCancelledUntilWrite()
{
$obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
$this->assertFalse($obj->isChanged('FirstName'));
$this->assertFalse($obj->isChanged('Surname'));
// Force change marks the records as changed
$obj->forceChange();
$this->assertTrue($obj->isChanged('FirstName'));
$this->assertTrue($obj->isChanged('Surname'));
// ...but not if we explicitly ask if the value has changed
$this->assertFalse($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertFalse($obj->isChanged('Surname', DataObject::CHANGE_VALUE));
// Not overwritten by setting the value to is original value
$obj->FirstName = 'Captain';
$this->assertTrue($obj->isChanged('FirstName'));
$this->assertTrue($obj->isChanged('Surname'));
// Not overwritten by changing it to something else and back again
$obj->FirstName = 'Captain-changed';
$this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$obj->FirstName = 'Captain';
$this->assertFalse($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertTrue($obj->isChanged('FirstName'));
$this->assertTrue($obj->isChanged('Surname'));
// Cleared after write
$obj->write();
$this->assertFalse($obj->isChanged('FirstName'));
$this->assertFalse($obj->isChanged('Surname'));
$obj->FirstName = 'Captain';
$this->assertFalse($obj->isChanged('FirstName'));
}
/**
* @skipUpgrade
*/
@ -1456,6 +1570,13 @@ class DataObjectTest extends SapphireTest
'Default Value',
'Defaults are populated from overloaded populateDefaults() method'
);
// Test populate defaults on subclasses
$staffObj = new DataObjectTest\Staff();
$this->assertEquals('Staff', $staffObj->EmploymentType);
$ceoObj = new DataObjectTest\CEO();
$this->assertEquals('Staff', $ceoObj->EmploymentType);
}
/**

View File

@ -9,6 +9,7 @@ class Staff extends DataObject implements TestOnly
{
private static $db = array(
'Salary' => 'BigInt',
'EmploymentType' => 'Varchar',
);
private static $table_name = 'DataObjectTest_Staff';
@ -17,4 +18,8 @@ class Staff extends DataObject implements TestOnly
'CurrentCompany' => Company::class,
'PreviousCompany' => Company::class
);
private static $defaults = [
'EmploymentType' => 'Staff',
];
}

View File

@ -187,48 +187,36 @@ class DatabaseTest extends SapphireTest
$this->assertTrue($db->canLock('DatabaseTest'), 'Can lock again after releasing it');
}
public function testTransactions()
public function testFieldTypes()
{
$conn = DB::get_conn();
if (!$conn->supportsTransactions()) {
$this->markTestSkipped("DB Doesn't support transactions");
return;
}
// Scaffold some data
$obj = new MyObject();
$obj->MyField = "value";
$obj->MyInt = 5;
$obj->MyFloat = 6.0;
$obj->MyBoolean = true;
$obj->write();
// Test that successful transactions are comitted
$obj = new DatabaseTest\MyObject();
$failed = false;
$conn->withTransaction(
function () use (&$obj) {
$obj->MyField = 'Save 1';
$obj->write();
},
function () use (&$failed) {
$failed = true;
}
);
$this->assertEquals('Save 1', DatabaseTest\MyObject::get()->first()->MyField);
$this->assertFalse($failed);
$record = DB::prepared_query(
'SELECT * FROM "DatabaseTest_MyObject" WHERE "ID" = ?',
[ $obj->ID ]
)->record();
// Test failed transactions are rolled back
$ex = null;
$failed = false;
try {
$conn->withTransaction(
function () use (&$obj) {
$obj->MyField = 'Save 2';
$obj->write();
throw new Exception("error");
},
function () use (&$failed) {
$failed = true;
}
);
} catch (Exception $ex) {
}
$this->assertTrue($failed);
$this->assertEquals('Save 1', DatabaseTest\MyObject::get()->first()->MyField);
$this->assertInstanceOf('Exception', $ex);
$this->assertEquals('error', $ex->getMessage());
// IDs and ints are returned as ints
$this->assertInternalType('int', $record['ID']);
$this->assertInternalType('int', $record['MyInt']);
$this->assertInternalType('float', $record['MyFloat']);
// Booleans are returned as ints  we follow MySQL's lead
$this->assertInternalType('int', $record['MyBoolean']);
// Strings and enums are returned as strings
$this->assertInternalType('string', $record['MyField']);
$this->assertInternalType('string', $record['ClassName']);
// Dates are returned as strings
$this->assertInternalType('string', $record['Created']);
$this->assertInternalType('string', $record['LastEdited']);
}
}

View File

@ -13,6 +13,9 @@ class MyObject extends DataObject implements TestOnly
private static $create_table_options = array(MySQLSchemaManager::ID => 'ENGINE=InnoDB');
private static $db = array(
'MyField' => 'Varchar'
'MyField' => 'Varchar',
'MyInt' => 'Int',
'MyFloat' => 'Float',
'MyBoolean' => 'Boolean',
);
}

View File

@ -0,0 +1,15 @@
<?php
namespace SilverStripe\ORM\Tests\FieldType;
use SilverStripe\ORM\DataObject;
class DBEnumTestObject extends DataObject
{
private static $table_name = 'FieldType_DBEnumTestObject';
private static $db = [
'Colour' => 'Enum("Red,Blue,Green")',
];
}

View File

@ -0,0 +1,278 @@
<?php
namespace SilverStripe\ORM\Tests;
use PHPUnit_Framework_MockObject_MockObject;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\ListDecorator;
use SilverStripe\ORM\SS_List;
/**
* This test class is testing that ListDecorator correctly proxies its calls through to the underlying SS_List
*/
class ListDecoratorTest extends SapphireTest
{
/**
* @var ArrayList|PHPUnit_Framework_MockObject_MockObject
*/
protected $list;
/**
* @var ListDecorator|PHPUnit_Framework_MockObject_MockObject
*/
protected $decorator;
protected function setUp()
{
parent::setUp();
$this->list = $this->createMock(ArrayList::class);
$this->decorator = $this->getMockForAbstractClass(ListDecorator::class, [$this->list]);
}
public function testGetIterator()
{
$this->list->expects($this->once())->method('getIterator')->willReturn('mock');
$this->assertSame('mock', $this->decorator->getIterator());
}
public function testCanSortBy()
{
$this->list->expects($this->once())->method('canSortBy')->with('foo')->willReturn(true);
$this->assertTrue($this->decorator->canSortBy('foo'));
}
public function testRemove()
{
$this->list->expects($this->once())->method('remove')->with('foo');
$this->decorator->remove('foo');
}
/**
* @param array $input
* @dataProvider filterProvider
*/
public function testExclude($input)
{
$this->list->expects($this->once())->method('exclude')->with($input)->willReturn('mock');
$this->assertSame('mock', $this->decorator->exclude($input));
}
/**
* @param array $input
* @dataProvider filterProvider
*/
public function testFilter($input)
{
$this->list->expects($this->once())->method('filter')->with($input)->willReturn('mock');
$this->assertSame('mock', $this->decorator->filter($input));
}
/**
* @param array $input
* @dataProvider filterProvider
*/
public function testFilterAny($input)
{
$this->list->expects($this->once())->method('filterAny')->with($input)->willReturn('mock');
$this->assertSame('mock', $this->decorator->filterAny($input));
}
/**
* @param array $input
* @dataProvider filterProvider
*/
public function testSort($input)
{
$this->list->expects($this->once())->method('sort')->with($input)->willReturn('mock');
$this->assertSame('mock', $this->decorator->sort($input));
}
/**
* @return array[]
*/
public function filterProvider()
{
return [
['Name', 'Bob'],
['Name', ['aziz', 'Bob']],
[['Name' =>'bob', 'Age' => 21]],
[['Name' =>'bob', 'Age' => [21, 43]]],
];
}
public function testCanFilterBy()
{
$this->list->expects($this->once())->method('canFilterBy')->with('Title')->willReturn(false);
$this->assertFalse($this->decorator->canFilterBy('Title'));
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage SS_Filterable::filterByCallback() passed callback must be callable, 'boolean' given
*/
public function testFilterByCallbackThrowsExceptionWhenGivenNonCallable()
{
$this->decorator->filterByCallback(true);
}
public function testFilterByCallback()
{
$input = new ArrayList([
['Name' => 'Leslie'],
['Name' => 'Maxime'],
['Name' => 'Sal'],
]);
$callback = function ($item, SS_List $list) {
return $item->Name === 'Maxime';
};
$this->decorator->setList($input);
$result = $this->decorator->filterByCallback($callback);
$this->assertCount(1, $result);
$this->assertSame('Maxime', $result->first()->Name);
}
public function testFind()
{
$this->list->expects($this->once())->method('find')->with('foo', 'bar')->willReturn('mock');
$this->assertSame('mock', $this->decorator->find('foo', 'bar'));
}
public function testDebug()
{
$this->list->expects($this->once())->method('debug')->willReturn('mock');
$this->assertSame('mock', $this->decorator->debug());
}
public function testCount()
{
$this->list->expects($this->once())->method('count')->willReturn(5);
$this->assertSame(5, $this->decorator->Count());
}
public function testEach()
{
$callable = function () {
// noop
};
$this->list->expects($this->once())->method('each')->with($callable)->willReturn('mock');
$this->assertSame('mock', $this->decorator->each($callable));
}
public function testOffsetExists()
{
$this->list->expects($this->once())->method('offsetExists')->with('foo')->willReturn('mock');
$this->assertSame('mock', $this->decorator->offsetExists('foo'));
}
public function testGetList()
{
$this->assertSame($this->list, $this->decorator->getList());
}
public function testColumnUnique()
{
$this->list->expects($this->once())->method('columnUnique')->with('ID')->willReturn('mock');
$this->assertSame('mock', $this->decorator->columnUnique('ID'));
}
public function testMap()
{
$this->list->expects($this->once())->method('map')->with('ID', 'Title')->willReturn('mock');
$this->assertSame('mock', $this->decorator->map('ID', 'Title'));
}
public function testReverse()
{
$this->list->expects($this->once())->method('reverse')->willReturn('mock');
$this->assertSame('mock', $this->decorator->reverse());
}
public function testOffsetGet()
{
$this->list->expects($this->once())->method('offsetGet')->with(2)->willReturn('mock');
$this->assertSame('mock', $this->decorator->offsetGet(2));
}
public function testExists()
{
$this->list->expects($this->once())->method('exists')->willReturn(false);
$this->assertFalse($this->decorator->exists());
}
public function testByID()
{
$this->list->expects($this->once())->method('byID')->with(123)->willReturn('mock');
$this->assertSame('mock', $this->decorator->byID(123));
}
public function testByIDs()
{
$this->list->expects($this->once())->method('byIDs')->with([1, 2])->willReturn('mock');
$this->assertSame('mock', $this->decorator->byIDs([1, 2]));
}
public function testToArray()
{
$this->list->expects($this->once())->method('toArray')->willReturn(['foo', 'bar']);
$this->assertSame(['foo', 'bar'], $this->decorator->toArray());
}
public function testToNestedArray()
{
$this->list->expects($this->once())->method('toNestedArray')->willReturn(['foo', 'bar']);
$this->assertSame(['foo', 'bar'], $this->decorator->toNestedArray());
}
public function testOffsetSet()
{
$this->list->expects($this->once())->method('offsetSet')->with('foo', 'bar');
$this->decorator->offsetSet('foo', 'bar');
}
public function testOffsetUnset()
{
$this->list->expects($this->once())->method('offsetUnset')->with('foo');
$this->decorator->offsetUnset('foo');
}
public function testLimit()
{
$this->list->expects($this->once())->method('limit')->with(5, 3)->willReturn('mock');
$this->assertSame('mock', $this->decorator->limit(5, 3));
}
public function testTotalItems()
{
$this->list->expects($this->once())->method('count')->willReturn(5);
$this->assertSame(5, $this->decorator->TotalItems());
}
public function testAdd()
{
$this->list->expects($this->once())->method('add')->with('foo')->willReturn('mock');
$this->decorator->add('foo');
}
public function testFirst()
{
$this->list->expects($this->once())->method('first')->willReturn(1);
$this->assertSame(1, $this->decorator->first());
}
public function testLast()
{
$this->list->expects($this->once())->method('last')->willReturn(10);
$this->assertSame(10, $this->decorator->last());
}
public function testColumn()
{
$this->list->expects($this->once())->method('column')->with('DOB')->willReturn('mock');
$this->assertSame('mock', $this->decorator->column('DOB'));
}
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\ORM\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\FieldType\DBMoney;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\Core\Convert;
@ -369,6 +370,46 @@ class ManyManyListTest extends SapphireTest
$this->assertSQLEquals($expected, $list->sql($parameters));
}
/**
* This tests that we can set a default sort on a join table, even though the class doesn't exist.
*
* @return void
*/
public function testSortByExtraFieldsDefaultSort()
{
$obj = new ManyManyListTest\ExtraFieldsObject();
$obj->write();
$obj2 = new ManyManyListTest\ExtraFieldsObject();
$obj2->write();
$money = new DBMoney();
$money->setAmount(100);
$money->setCurrency('USD');
// Add two objects as relations (first is linking back to itself)
$obj->Clients()->add($obj, ['Worth' => $money, 'Reference' => 'A']);
$obj->Clients()->add($obj2, ['Worth' => $money, 'Reference' => 'B']);
// Set the default sort for this relation
Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference ASC');
$clients = $obj->Clients();
$this->assertCount(2, $clients);
list($first, $second) = $obj->Clients();
$this->assertEquals('A', $first->Reference);
$this->assertEquals('B', $second->Reference);
// Now we ensure the default sort is being respected by reversing its order
Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference DESC');
$reverseClients = $obj->Clients();
$this->assertCount(2, $reverseClients);
list($reverseFirst, $reverseSecond) = $obj->Clients();
$this->assertEquals('B', $reverseFirst->Reference);
$this->assertEquals('A', $reverseSecond->Reference);
}
public function testFilteringOnPreviouslyJoinedTable()
{
/** @var ManyManyListTest\Category $category */

View File

@ -2,11 +2,14 @@
namespace SilverStripe\ORM\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale;
class ManyManyThroughListTest extends SapphireTest
{
@ -20,6 +23,8 @@ class ManyManyThroughListTest extends SapphireTest
ManyManyThroughListTest\PolyJoinObject::class,
ManyManyThroughListTest\PolyObjectA::class,
ManyManyThroughListTest\PolyObjectB::class,
ManyManyThroughListTest\Locale::class,
ManyManyThroughListTest\FallbackLocale::class,
];
protected function setUp()
@ -320,4 +325,32 @@ class ManyManyThroughListTest extends SapphireTest
$this->assertEquals($joinTable, $objB1->Items()->getJoinTable());
$this->assertEquals($joinTable, $objB2->Items()->getJoinTable());
}
/**
* This tests that default sort works when the join table has a default sort set, and the main
* dataobject has a default sort set.
*
* @return void
*/
public function testDefaultSortOnJoinAndMain()
{
// We have spanish mexico with two fall back locales; argentina and international sorted in that order.
$mexico = $this->objFromFixture(Locale::class, 'mexico');
$fallbacks = $mexico->Fallbacks();
$this->assertCount(2, $fallbacks);
// Ensure the default sort is is correct
list($first, $second) = $fallbacks;
$this->assertSame('Argentina', $first->Title);
$this->assertSame('International', $second->Title);
// Ensure that we're respecting the default sort by reversing it
Config::inst()->update(FallbackLocale::class, 'default_sort', '"ManyManyThroughTest_FallbackLocale"."Sort" DESC');
$reverse = $mexico->Fallbacks();
list($firstReverse, $secondReverse) = $reverse;
$this->assertSame('International', $firstReverse->Title);
$this->assertSame('Argentina', $secondReverse->Title);
}
}

View File

@ -51,3 +51,28 @@ SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject:
Sort: 2
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectB.objb2
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem.child2
SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale:
international:
Title: 'International'
Locale: 'en_NZ'
URLSegment: 'international'
IsGlobalDefault: 1
mexico:
Title: 'Mexico'
Locale: 'es_MX'
URLSegment: 'mexico'
IsGlobalDefault: 0
argentina:
Title: 'Argentina'
Locale: 'es_AR'
URLSegment: 'argentina'
IsGlobalDefault: 0
SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale:
mexico_international:
Sort: 2
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.mexico
Locale: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.international
mexico_argentina:
Sort: 1
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.mexico
Locale: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.argentina

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\TestOnly;
class FallbackLocale extends DataObject implements TestOnly
{
private static $db = [
'Sort' => 'Int',
];
private static $has_one = [
'Parent' => Locale::class,
'Locale' => Locale::class,
];
private static $table_name = 'ManyManyThroughTest_FallbackLocale';
private static $default_sort = 'Sort';
}

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\TestOnly;
class Locale extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughTest_Locale';
/**
* @config
* @var array
*/
private static $db = [
'Title' => 'Varchar(100)',
'Locale' => 'Varchar(10)',
'URLSegment' => 'Varchar(100)',
'IsGlobalDefault' => 'Boolean',
];
private static $has_many = [
'FallbackLocales' => FallbackLocale::class . '.Parent',
];
private static $many_many = [
'Fallbacks' => [
'through' => FallbackLocale::class,
'from' => 'Parent',
'to' => 'Locale',
],
];
private static $default_sort = '"ManyManyThroughTest_Locale"."Locale" ASC';
}

View File

@ -5,16 +5,33 @@ namespace SilverStripe\ORM\Tests;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\Tests\TransactionTest\TestObject;
class TransactionTest extends SapphireTest
{
protected $usesDatabase = true;
protected $usesTransactions = false;
protected static $extra_dataobjects = [
TransactionTest\TestObject::class,
];
private static $originalVersionInfo;
protected function setUp()
{
parent::setUp();
self::$originalVersionInfo = Deprecation::dump_settings();
}
protected function tearDown()
{
Deprecation::restore_settings(self::$originalVersionInfo);
parent::tearDown();
}
public static function setUpBeforeClass()
{
parent::setUpBeforeClass();
@ -23,8 +40,57 @@ class TransactionTest extends SapphireTest
}
}
public function testTransactions()
{
$conn = DB::get_conn();
if (!$conn->supportsTransactions()) {
$this->markTestSkipped("DB Doesn't support transactions");
return;
}
// Test that successful transactions are comitted
$obj = new TestObject();
$failed = false;
$conn->withTransaction(
function () use (&$obj) {
$obj->Title = 'Save 1';
$obj->write();
},
function () use (&$failed) {
$failed = true;
}
);
$this->assertEquals('Save 1', TestObject::get()->first()->Title);
$this->assertFalse($failed);
// Test failed transactions are rolled back
$ex = null;
$failed = false;
try {
$conn->withTransaction(
function () use (&$obj) {
$obj->Title = 'Save 2';
$obj->write();
throw new \Exception("error");
},
function () use (&$failed) {
$failed = true;
}
);
} catch (\Exception $ex) {
}
$this->assertTrue($failed);
$this->assertEquals('Save 1', TestObject::get()->first()->Title);
$this->assertInstanceOf('Exception', $ex);
$this->assertEquals('error', $ex->getMessage());
}
public function testNestedTransaction()
{
if (!DB::get_conn()->supportsSavepoints()) {
static::markTestSkipped('Current database does not support savepoints');
}
$this->assertCount(0, TestObject::get());
try {
DB::get_conn()->withTransaction(function () {
@ -51,6 +117,7 @@ class TransactionTest extends SapphireTest
public function testCreateWithTransaction()
{
// First/Second in a successful transaction
DB::get_conn()->transactionStart();
$obj = new TransactionTest\TestObject();
$obj->Title = 'First page';
@ -59,10 +126,10 @@ class TransactionTest extends SapphireTest
$obj = new TransactionTest\TestObject();
$obj->Title = 'Second page';
$obj->write();
DB::get_conn()->transactionEnd();
//Create a savepoint here:
DB::get_conn()->transactionSavepoint('rollback');
// Third/Fourth in a rolled back transaction
DB::get_conn()->transactionStart();
$obj = new TransactionTest\TestObject();
$obj->Title = 'Third page';
$obj->write();
@ -70,11 +137,8 @@ class TransactionTest extends SapphireTest
$obj = new TransactionTest\TestObject();
$obj->Title = 'Fourth page';
$obj->write();
DB::get_conn()->transactionRollback();
//Revert to a savepoint:
DB::get_conn()->transactionRollback('rollback');
DB::get_conn()->transactionEnd();
$first = DataObject::get(TransactionTest\TestObject::class, "\"Title\"='First page'");
$second = DataObject::get(TransactionTest\TestObject::class, "\"Title\"='Second page'");
@ -85,8 +149,48 @@ class TransactionTest extends SapphireTest
$this->assertTrue(is_object($first) && $first->exists());
$this->assertTrue(is_object($second) && $second->exists());
//These pages should NOT exist, we reverted to a savepoint:
//These pages should NOT exist, we rolled back
$this->assertFalse(is_object($third) && $third->exists());
$this->assertFalse(is_object($fourth) && $fourth->exists());
}
public function testReadOnlyTransaction()
{
if (!DB::get_conn()->supportsTransactions()) {
$this->markTestSkipped('Current database is doesn\'t support transactions');
return;
}
// This feature is deprecated in 4.4, but we're still testing it.
Deprecation::notification_version('4.3.0');
$page = new TestObject();
$page->Title = 'Read only success';
$page->write();
DB::get_conn()->transactionStart('READ ONLY');
try {
$page = new TestObject();
$page->Title = 'Read only page failed';
$page->write();
DB::get_conn()->transactionEnd();
} catch (\Exception $e) {
//could not write this record
//We need to do a rollback or a commit otherwise we'll get error messages
DB::get_conn()->transactionRollback();
}
DataObject::flush_and_destroy_cache();
$success = DataObject::get_one(TestObject::class, "\"Title\"='Read only success'");
$fail = DataObject::get_one(TestObject::class, "\"Title\"='Read only page failed'");
//This page should be in the system
$this->assertInternalType('object', $success);
$this->assertTrue($success->exists());
//This page should NOT exist, we had 'read only' permissions
$this->assertNull($fail);
}
}

View File

@ -18,6 +18,7 @@ use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
use SilverStripe\Security\PasswordValidator;
use SilverStripe\Security\Security;
/**
@ -44,6 +45,10 @@ class MemberAuthenticatorTest extends SapphireTest
$this->defaultPassword = null;
}
DefaultAdminService::setDefaultAdmin('admin', 'password');
PasswordValidator::singleton()
->setMinLength(0)
->setTestNames([]);
}
protected function tearDown()

View File

@ -6,6 +6,7 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Group;
use SilverStripe\Security\MemberCsvBulkLoader;
use SilverStripe\Security\Member;
use SilverStripe\Security\PasswordValidator;
use SilverStripe\Security\Security;
use SilverStripe\Dev\SapphireTest;
@ -13,6 +14,15 @@ class MemberCsvBulkLoaderTest extends SapphireTest
{
protected static $fixture_file = 'MemberCsvBulkLoaderTest.yml';
protected function setUp()
{
parent::setUp();
PasswordValidator::singleton()
->setMinLength(0)
->setTestNames([]);
}
public function testNewImport()
{
$loader = new MemberCsvBulkLoader();

View File

@ -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,10 @@ class MemberTest extends FunctionalTest
parent::setUp();
Member::config()->set('unique_identifier_field', 'Email');
Member::set_password_validator(null);
PasswordValidator::singleton()
->setMinLength(0)
->setTestNames([]);
i18n::set_locale('en_US');
}
@ -655,10 +659,14 @@ class MemberTest extends FunctionalTest
$member = $this->objFromFixture(Member::class, 'test');
$member->setName('Test Some User');
$this->assertEquals('Test Some User', $member->getName());
$this->assertEquals('Test Some', $member->FirstName);
$this->assertEquals('User', $member->Surname);
$member->setName('Test');
$this->assertEquals('Test', $member->getName());
$member->FirstName = 'Test';
$member->Surname = '';
$this->assertEquals('Test', $member->FirstName);
$this->assertEquals('', $member->Surname);
$this->assertEquals('Test', $member->getName());
}

View File

@ -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();
PasswordValidator::config()
->remove('min_length')
->remove('historic_count')
->set('min_test_score', 0);
}
public function testValidate()
{
$v = new PasswordValidator();

View File

@ -14,13 +14,14 @@ use SilverStripe\Dev\FunctionalTest;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
use SilverStripe\Security\PasswordValidator;
use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken;
@ -51,6 +52,13 @@ class SecurityTest extends FunctionalTest
*/
Member::config()->set('unique_identifier_field', 'Email');
PasswordValidator::config()
->remove('min_length')
->remove('historic_count')
->remove('min_test_score');
Member::set_password_validator(null);
parent::setUp();
Director::config()->set('alternate_base_url', '/');
@ -388,7 +396,7 @@ class SecurityTest extends FunctionalTest
// Test external redirection on ChangePasswordForm
$this->get('Security/changepassword?BackURL=http://myspoofedhost.com');
$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123');
$this->assertNotRegExp(
'/^' . preg_quote('http://myspoofedhost.com', '/') . '/',
(string)$changedResponse->getHeader('Location'),
@ -435,7 +443,7 @@ class SecurityTest extends FunctionalTest
// Make sure it redirects correctly after the password has been changed
$this->mainSession->followRedirection();
$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123');
$this->assertEquals(302, $changedResponse->getStatusCode());
$this->assertEquals(
Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
@ -449,7 +457,7 @@ class SecurityTest extends FunctionalTest
// Change the password
$this->get('Security/changepassword?BackURL=test/back');
$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123');
$this->assertEquals(302, $changedResponse->getStatusCode());
$this->assertEquals(
Controller::join_links(Director::absoluteBaseURL(), 'test/back'),
@ -459,7 +467,7 @@ class SecurityTest extends FunctionalTest
// Check if we can login with the new password
$this->logOut();
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword#123');
$this->assertEquals(302, $goodResponse->getStatusCode());
$this->assertEquals(
Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
@ -501,12 +509,12 @@ class SecurityTest extends FunctionalTest
// Follow redirection to form without hash in GET parameter
$this->get('Security/changepassword');
$this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
$this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123');
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs'));
// Check if we can login with the new password
$this->logOut();
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword#123');
$this->assertEquals(302, $goodResponse->getStatusCode());
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs'));
@ -671,7 +679,7 @@ class SecurityTest extends FunctionalTest
public function testDatabaseIsReadyWithInsufficientMemberColumns()
{
Security::clear_database_is_ready();
DBClassName::clear_classname_cache();
DBEnum::flushCache();
// Assumption: The database has been built correctly by the test runner,
// and has all columns present in the ORM

View File

@ -80,4 +80,19 @@ class HTML4ValueTest extends SapphireTest
$value->setContent('<a href="&quot;"></a>');
$this->assertEquals('<a href="&quot;"></a>', $value->getContent(), "'\"' character is escaped");
}
public function testGetContent()
{
$value = new HTML4Value();
$value->setContent('<p>This is valid</p>');
$this->assertEquals('<p>This is valid</p>', $value->getContent(), "Valid content is returned");
$value->setContent('<p?< This is not really valid but it will get parsed into something valid');
// can sometimes get a this state where HTMLValue->valid is false
// for instance if a content editor saves something really weird in a LiteralField
// we can manually get to this state via ->setInvalid()
$value->setInvalid();
$this->assertEquals('', $value->getContent(), "Blank string is returned when invalid");
}
}

View File

@ -6,6 +6,7 @@ use InvalidArgumentException;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n;
use SilverStripe\View\Requirements;
use SilverStripe\View\ArrayData;
use Silverstripe\Assets\Dev\TestAssetStore;
@ -1109,4 +1110,77 @@ EOS
}
return array();
}
public function testAddI18nJavascript()
{
/** @var Requirements_Backend $backend */
$backend = Injector::inst()->create(Requirements_Backend::class);
$this->setupRequirements($backend);
$backend->add_i18n_javascript('i18n');
$actual = $backend->getJavascript();
// English and English US should always be loaded no matter what
$this->assertArrayHasKey('i18n/en.js', $actual);
$this->assertArrayHasKey('i18n/en_US.js', $actual);
$this->assertArrayHasKey('i18n/en-us.js', $actual);
}
public function testAddI18nJavascriptWithDefaultLocale()
{
i18n::config()->set('default_locale', 'fr_CA');
/** @var Requirements_Backend $backend */
$backend = Injector::inst()->create(Requirements_Backend::class);
$this->setupRequirements($backend);
$backend->add_i18n_javascript('i18n');
$actual = $backend->getJavascript();
$this->assertArrayHasKey('i18n/en.js', $actual);
$this->assertArrayHasKey('i18n/en_US.js', $actual);
$this->assertArrayHasKey('i18n/en-us.js', $actual);
// Default locale should be loaded
$this->assertArrayHasKey('i18n/fr.js', $actual);
$this->assertArrayHasKey('i18n/fr_CA.js', $actual);
$this->assertArrayHasKey('i18n/fr-ca.js', $actual);
}
public function testAddI18nJavascriptWithMemberLocale()
{
i18n::set_locale('en_GB');
/** @var Requirements_Backend $backend */
$backend = Injector::inst()->create(Requirements_Backend::class);
$this->setupRequirements($backend);
$backend->add_i18n_javascript('i18n');
$actual = $backend->getJavascript();
// The current member's Locale as defined by i18n::get_locale should be loaded
$this->assertArrayHasKey('i18n/en.js', $actual);
$this->assertArrayHasKey('i18n/en_US.js', $actual);
$this->assertArrayHasKey('i18n/en-us.js', $actual);
$this->assertArrayHasKey('i18n/en-gb.js', $actual);
$this->assertArrayHasKey('i18n/en_GB.js', $actual);
}
public function testAddI18nJavascriptWithMissingLocale()
{
i18n::set_locale('fr_BE');
/** @var Requirements_Backend $backend */
$backend = Injector::inst()->create(Requirements_Backend::class);
$this->setupRequirements($backend);
$backend->add_i18n_javascript('i18n');
$actual = $backend->getJavascript();
// We don't have a file for French Belgium. Regular french should be loaded anyway.
$this->assertArrayHasKey('i18n/en.js', $actual);
$this->assertArrayHasKey('i18n/en_US.js', $actual);
$this->assertArrayHasKey('i18n/en-us.js', $actual);
$this->assertArrayHasKey('i18n/fr.js', $actual);
}
}

View File

View File

View File

View File

@ -102,7 +102,7 @@ class i18nTest extends SapphireTest
$obj->fieldLabel('MyProperty')
);
$this->assertEquals(
'My Untranslated Property',
'My untranslated property',
$obj->fieldLabel('MyUntranslatedProperty')
);
}