Merge branch '4.3' into 4.4

This commit is contained in:
Aaron Carlino 2019-06-10 17:32:07 +12:00
commit c747b1f8d3
79 changed files with 4061 additions and 1300 deletions

View File

@ -0,0 +1,35 @@
---
Name: confirmation_middleware-prototypes
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Middleware\ConfirmationMiddleware\AjaxBypass:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\AjaxBypass
type: prototype
SilverStripe\Control\Middleware\ConfirmationMiddleware\GetParameter:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\GetParameter
type: prototype
SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith
type: prototype
SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswithCaseInsensitive:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswithCaseInsensitive
type: prototype
SilverStripe\Control\Middleware\ConfirmationMiddleware\EnvironmentBypass:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\EnvironmentBypass
type: prototype
SilverStripe\Control\Middleware\ConfirmationMiddleware\CliBypass:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\CliBypass
type: prototype
SilverStripe\Control\Middleware\ConfirmationMiddleware\HttpMethodBypass:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\HttpMethodBypass
type: prototype
SilverStripe\Control\Middleware\ConfirmationMiddleware\Url:
class: SilverStripe\Control\Middleware\ConfirmationMiddleware\Url
type: prototype

View File

@ -11,3 +11,5 @@ SilverStripe\Dev\DevelopmentAdmin:
controller: SilverStripe\Dev\TaskRunner controller: SilverStripe\Dev\TaskRunner
links: links:
tasks: 'See a list of build tasks to run' tasks: 'See a list of build tasks to run'
confirm:
controller: SilverStripe\Dev\DevConfirmationController

View File

@ -32,6 +32,7 @@ SilverStripe\Core\Injector\Injector:
RequestHandler: '%$SilverStripe\Security\Security' RequestHandler: '%$SilverStripe\Security\Security'
Middlewares: Middlewares:
- '%$SecurityRateLimitMiddleware' - '%$SecurityRateLimitMiddleware'
--- ---
Name: errorrequestprocessors Name: errorrequestprocessors
After: After:
@ -40,6 +41,8 @@ After:
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
# Note: If Director config changes, take note it will affect this config too # Note: If Director config changes, take note it will affect this config too
SilverStripe\Core\Startup\ErrorDirector: '%$SilverStripe\Control\Director' SilverStripe\Core\Startup\ErrorDirector: '%$SilverStripe\Control\Director'
--- ---
Name: canonicalurls Name: canonicalurls
--- ---
@ -48,3 +51,94 @@ SilverStripe\Core\Injector\Injector:
properties: properties:
ForceSSL: false ForceSSL: false
ForceWWW: false ForceWWW: false
---
Name: url_specials-middleware
After:
- 'requestprocessors'
- 'coresecurity'
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director:
properties:
Middlewares:
URLSpecialsMiddleware: '%$SilverStripe\Control\Middleware\URLSpecialsMiddleware'
SilverStripe\Control\Middleware\URLSpecialsMiddleware:
class: SilverStripe\Control\Middleware\URLSpecialsMiddleware
properties:
ConfirmationStorageId: 'url-specials'
ConfirmationFormUrl: '/dev/confirm'
Bypasses:
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\CliBypass'
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\EnvironmentBypass("dev")'
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith("dev/confirm")'
EnforceAuthentication: true
AffectedPermissions:
- ADMIN
---
Name: dev_urls-confirmation-middleware
After:
- 'url_specials-middleware'
---
# This middleware enforces confirmation (CSRF protection) for all URLs
# that start with "dev/*", with the exception for "dev/build" which is handled
# by url_specials-middleware
# If you want to make exceptions for some URLs,
# see "dev_urls-confirmation-exceptions" config
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director:
properties:
Middlewares:
DevUrlsConfirmationMiddleware: '%$DevUrlsConfirmationMiddleware'
DevUrlsConfirmationMiddleware:
class: SilverStripe\Control\Middleware\PermissionAwareConfirmationMiddleware
constructor:
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith("dev")'
properties:
ConfirmationStorageId: 'dev-urls'
ConfirmationFormUrl: '/dev/confirm'
Bypasses:
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\CliBypass'
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\EnvironmentBypass("dev")'
EnforceAuthentication: false
AffectedPermissions:
- ADMIN
---
Name: dev_urls-confirmation-exceptions
After:
- 'dev_urls-confirmation-middleware'
---
# This config is the place to add custom bypasses for modules providing UIs
# on top of DevelopmentAdmin (dev/*)
# If the module has its own CSRF protection, the easiest way would be to
# simply add UrlPathStartswith with the path to the mount point.
# Example:
# # This will prevent confirmation for all URLs starting with "dev/custom-module-endpoint/"
# # WARNING: this won't prevent confirmation for "dev/custom-module-endpoint-suffix/"
# - '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith("dev/custom-module-endpoint")'
# If the module does not implement its own CSRF protection but exposes all
# dangerous effects through POST, then you could simply exclude GET and HEAD requests
# by using HttpMethodBypass("GET", "HEAD"). In that case GET/HEAD requests will not
# trigger confirmation redirects.
SilverStripe\Core\Injector\Injector:
DevUrlsConfirmationMiddleware:
properties:
Bypasses:
# dev/build is covered by URLSpecialsMiddleware
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith("dev/build")'
# The confirmation form is where people will be redirected for confirmation. We don't want to block it.
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith("dev/confirm")'
# Allows GET requests to the dev index page
- '%$SilverStripe\Control\Middleware\ConfirmationMiddleware\Url("dev", ["GET", "HEAD"])'

View File

@ -114,3 +114,4 @@ SilverStripe core environment variables are listed here, though you're free to d
| `SS_DATABASE_SSL_CERT` | Absolute path to SSL certificate file | | `SS_DATABASE_SSL_CERT` | Absolute path to SSL certificate file |
| `SS_DATABASE_SSL_CA` | Absolute path to SSL Certificate Authority bundle file | | `SS_DATABASE_SSL_CA` | Absolute path to SSL Certificate Authority bundle file |
| `SS_DATABASE_SSL_CIPHER` | Optional setting for custom SSL cipher | | `SS_DATABASE_SSL_CIPHER` | Optional setting for custom SSL cipher |
| `SS_FLUSH_ON_DEPLOY` | Try to detect deployments through file system modifications and flush on the first request after every deploy. Does not run "dev/build", but only "flush". Possible values are `true` (check for a framework PHP file modification time), `false` (no checks, skip deploy detection) or a path to a specific file or folder to be checked. See [DeployFlushDiscoverer](api:SilverStripe\Core\Startup\DeployFlushDiscoverer) for more details.<br /><br />False by default. |

View File

@ -118,4 +118,5 @@ SilverStripe\Control\Director:
## API Documentation ## API Documentation
* [Built-in Middleware](./06_Builtin_Middlewares.md)
* [HTTPMiddleware](api:SilverStripe\Control\Middleware\HTTPMiddleware) * [HTTPMiddleware](api:SilverStripe\Control\Middleware\HTTPMiddleware)

View File

@ -0,0 +1,21 @@
title: Built-in Middleware
summary: Middleware components that come with SilverStripe Framework
# Built-in Middleware
SilverStripe Framework has a number of Middleware components.
You may find them in the [SilverStripe\Control\Middleware](api:SilverStripe\Control\Middleware) namespace.
| Name | Description |
| ---- | ----------- |
| [AllowedHostsMiddleware](api:SilverStripe\Control\Middleware\AllowedHostsMiddleware) | Secures requests by only allowing a whitelist of Host values |
| [CanonicalURLMiddleware](api:SilverStripe\Control\Middleware\CanonicalURLMiddleware) | URL normalisation and redirection |
| [ChangeDetectionMiddleware](api:SilverStripe\Control\Middleware\ChangeDetectionMiddleware) | Change detection via Etag / IfModifiedSince headers, conditionally sending a 304 not modified if possible. |\
| [ConfirmationMiddleware](api:SilverStripe\Control\Middleware\ConfirmationMiddleware) | Checks whether user manual confirmation is required for HTTPRequest |
| [ExecMetricMiddleware](api:SilverStripe\Control\Middleware\ExecMetricMiddleware) | Display execution metrics in DEV mode |
| [FlushMiddleware](api:SilverStripe\Control\Middleware\FlushMiddleware) | Triggers a call to flush() on all [Flushable](api:SilverStripe\Core\Flushable) implementors |
| [HTTPCacheControlMiddleware](api:SilverStripe\Control\Middleware\HTTPCacheControlMiddleware) | Controls HTTP response cache headers |
| [RateLimitMiddleware](api:SilverStripe\Control\Middleware\RateLimitMiddleware) | Access throttling, controls HTTP Retry-After header |
| [SessionMiddleware](api:SilverStripe\Control\Middleware\SessionMiddleware) | PHP Session initialisation |
| [TrustedProxyMiddleware](api:SilverStripe\Control\Middleware\TrustedProxyMiddleware) | Rewrites headers that provide IP and host details from upstream proxies |
| [URLSpecialsMiddleware](api:SilverStripe\Control\Middleware\URLSpecialsMiddleware) | Controls some of the [URL special variables](../../debugging/url_variable_tools) |

View File

@ -29,7 +29,6 @@ from the `silverstripe/admin` module
into `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss`. It will automatically be picked up by 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">` the CMS logic. Add a new section into the `<ul class="cms-menu__list">`
```ss ```ss
... ...
<ul class="cms-menu-list"> <ul class="cms-menu-list">
@ -243,7 +242,7 @@ how-to.
## React-rendered UI ## React-rendered UI
For sections of the admin that are rendered with React, Redux, and GraphQL, please refer For sections of the admin that are rendered with React, Redux, and GraphQL, please refer
to [the introduction on those concepts](../07_ReactJS_Redux_and_GraphQL.md), to [the introduction on those concepts](../../reactjs_redux_and_graphql/),
as well as their respective How-To's in this section. as well as their respective How-To's in this section.
### Implementing handlers ### Implementing handlers

View File

@ -9,6 +9,14 @@ Allows a class to define it's own flush functionality, which is triggered when `
[FlushMiddleware](api:SilverStripe\Control\Middleware\FlushMiddleware) is run before a request is made, calling `flush()` statically on all [FlushMiddleware](api:SilverStripe\Control\Middleware\FlushMiddleware) is run before a request is made, calling `flush()` statically on all
implementors of [Flushable](api:SilverStripe\Core\Flushable). implementors of [Flushable](api:SilverStripe\Core\Flushable).
<div class="notice">
Flushable implementers might also be triggered automatically on deploy if you have `SS_FLUSH_ON_DEPLOY` [environment
variable](../configuration/environment_variables) defined. In that case even if you don't manually pass `flush=1` parameter, the first request after deploy
will still be calling `Flushable::flush` on those entities.
</div>
## Usage ## Usage
To use this API, you need to make your class implement [Flushable](api:SilverStripe\Core\Flushable), and define a `flush()` static function, To use this API, you need to make your class implement [Flushable](api:SilverStripe\Core\Flushable), and define a `flush()` static function,

View File

@ -74,6 +74,11 @@ sake dev/
sake dev/build "flush=1" sake dev/build "flush=1"
``` ```
<div class="alert" markdown="1">
You have to run "sake" with the same system user that runs your web server,
otherwise "flush" won't be able to clean the cache properly.
</div>
It can also be handy if you have a long running script.. It can also be handy if you have a long running script..
```bash ```bash

View File

@ -1,9 +1,10 @@
# 4.4.0 (Unreleased) # 4.4.0
## Overview {#overview} ## Overview {#overview}
- [Optional migration to hash-less public asset URLs](#hash-less) - [Optional migration to hash-less public asset URLs](#hash-less)
- [Optional migration of legacy thumbnail locations](#legacy-thumb) - [Optional migration of legacy thumbnail locations](#legacy-thumb)
- Security patch for [SS-2018-022](https://www.silverstripe.org/download/security-releases/ss-2018-022)
- [Correct PHP types are now returned from database queries](/developer_guides/model/sql_select#data-types) - [Correct PHP types are now returned from database queries](/developer_guides/model/sql_select#data-types)
- [Server Requirements](/getting_started/server_requirements/#web-server-software-requirements) have been refined: - [Server Requirements](/getting_started/server_requirements/#web-server-software-requirements) have been refined:
MySQL 5.5 end of life reached in December 2018, thus SilverStripe 4.4 requires MySQL 5.6+. MySQL 5.5 end of life reached in December 2018, thus SilverStripe 4.4 requires MySQL 5.6+.
@ -13,8 +14,29 @@
- Removed `File.migrate_legacy_file` config option. Migration tasks now need to run via `dev/tasks/`, - Removed `File.migrate_legacy_file` config option. Migration tasks now need to run via `dev/tasks/`,
running them as part of `dev/build` is no longer supported running them as part of `dev/build` is no longer supported
### DevelopmentAdmin controllers
On Live environment all browser based HTTP requests to `/dev/*` urls get redirected to a confirmation form.
See more details below in the Upgrading section.
### DevelopmentAdmin cli-only mode
DevelopmentAdmin now has CLI-only mode (off by default).
The mode makes all `dev/*` controllers to be only accessible from CLI (e.g. sake).
To turn it on you may add the following configuration to your project configs:
```yml
SilverStripe\Dev\DevelopmentAdmin:
deny_non_cli: true
```
## Upgrading {#upgrading} ## Upgrading {#upgrading}
### If you are migrating files from SilverStripe 3
Youll want to use the 4.4.1 release if youre migrating files from SilverStripe 3 to 4.4, as this concurrent patch release contains critical bug fixes and optimisations for file migrations.
### Adopting to new `_resources` directory ### Adopting to new `_resources` directory
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`. 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`.
@ -216,8 +238,307 @@ Yes, it will attempt to find the most recent public "hash-less" URL
for this file and redirect to it. for this file and redirect to it.
### DevelopmentAdmin controllers
The security fix for [SS-2018-022](https://www.silverstripe.org/download/security-releases/ss-2018-022) introduces a new
[Confirmation](api:SilverStripe\Security\Confirmation) component and
[ConfirmationMiddleware](api:SilverStripe\Control\Middleware\ConfirmationMiddleware) that prevents CSRF based attacks
on the urls placed under `dev/*` path.
If you use `dev/` endpoints, you may need to consider the following changes:
- `/dev/confirm` url now holds the confirmation form, where users will have to manually approve their actions
- on live environments all non-cli (browser based) HTTP requests to `dev/*` urls get redirected to the confirmation form
- ajax requests to `/dev/*` urls will be redirected as well (as such may stop working until configuration is added)
- `GET` and `POST` requests are handled gracefully, but other HTTP methods will be transformed to `GET` by the confirmation form
- you may add custom configuration for the confirmation middleware to prevent redirection for some requests or URLs
CLI based requests (e.g. sake) are not affected by the confirmation middleware and keep working as is.
If you are a 3rd party module developer and you extend DevelopmentAdmin adding new routes under the `dev/` path, you may
need to add custom rules for the confirmation middleware in your module configuration. Otherwise, people navigating
those through browsers will have to confirm every single action, which may impair user experience significantly.
You may find a configuration example in the framework `_config/requestprocessors.yml`
file, named `dev_urls-confirmation-exceptions`.
### ErrorControlChainMiddleware is deactivated
ErrorControlChainMiddleware has been deactivated and deprecated. It is going to be removed in SilverStripe 5.0.
That means uncaught exceptions and fatal errors will no longer trigger `flush` on live environments.
The main historic purpose of ErrorControlChainMiddleware was to detect the application state in which manifest cache is
incompatible with the source code, which might lead to fatal errors or unexpected exceptions. The only way this can
happen is when manifest cache has been generated and application source code files change afterwards.
The only reasonable cause for that on live environments would be application deployment.
Ideally, you should avoid reusing manifest cache between different application deploys, so that every newly deployed
version generates its own manifest cache. However, if that's not the case, you may want to consider using
`SS_FLUSH_ON_DEPLOY` setting, which automatically triggers `flush` on every deploy by comparing filesystem modification
time with cache generation timestamp. This effectively eliminates the possibility for the manifest cache to be incompatible
with the deployed app.
<div class="alert" markdown="1">
WARNING! If you do not deploy your application as a whole, but rather update its files in place with `rsync`, `ssh`
or `FTP`, you should consider triggering CLI based `flush` manually on every such deploy (e.g. with sake).
Otherwise, you may end up with your application cache to be incompatible with its source code, which would make things
broken. There is a tiny possibility that you wouldn't even be able to flush through browser in that case.
<br />
In that case you may consider using `SS_FLUSH_ON_DEPLOY`. Depending on your deployment tooling you may point it to a filesystem resource
that gets modified on every deploy update so that the framework will automatically perform `flush` for you.
<br />
The best practice is not to reuse the application manifest cache between deploys.
</div>
## Changes to internal APIs ## Changes to internal APIs
- `PDOQuery::__construct()` now has a 2nd argument. If you have subclassed PDOQuery and overridden __construct() - `PDOQuery::__construct()` now has a 2nd argument. If you have subclassed PDOQuery and overridden __construct()
you may see an E_STRICT error 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. - 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.
- `SilverStripe\Control\HTTPApplication` now uses `FlushDiscoverer` implementers to check for `flush`
- `SilverStripe\Control\Middleware\ConfirmationMiddleware` component implemented
- `SilverStripe\Control\Middleware\URLSpecialsMiddleware` component implemented
- `SilverStripe\Control\Director::isManifestFlushed` static function implemented
- `SilverStripe\Core\CoreKernel::isFlushed` function keeps boolean whether manifest cache has been flushed
- `SilverStripe\Core\Environment::isCli` method is now responsible for low level CLI detection (on before the kernel boot stage).
`Director::is_cli` is still to be used on the application level.
- `SilverStripe\Core\Startup\ErrorControlChainMiddleware::__construct` has a 2nd argument which activates its legacy behaviour
- `SilverStripe\Core\Startup\FlushDiscoverer` interface and a number of its implementations in the same namespace
- `SilverStripe\Dev\DevConfirmationController` implements the confirmation form for the `/dev/confirm` endpoint
- `SilverStripe\Dev\DevelopmentAdmin` now has `deny_non_cli` configuration parameter
- `SilverStripe\Security\Confirmation` component implemented
## Deprecations
- `SilverStripe\Control\Director::isManifestFlushed`
- `SilverStripe\Core\CoreKernel::getEnvironment`
- `SilverStripe\Core\CoreKernel::sessionEnvironment`
- `SilverStripe\Core\Startup\AbstractConfirmationToken`
- `SilverStripe\Core\Startup\ConfirmationTokenChain`
- `SilverStripe\Core\Startup\ErrorControlChain`
- `SilverStripe\Core\Startup\ErrorControlChainMiddleware`
- `SilverStripe\Core\Startup\ErrorDirector`
- `SilverStripe\Core\Startup\ParameterConfirmationToken`
- `SilverStripe\Core\Startup\URLConfirmationToken`
## Change Log
### Security
* 2019-06-10 [bea3f0205](https://github.com/silverstripe/silverstripe-graphql/commit/bea3f0205e1c1e48b39ee139daa9fe223e05cf0d) [CVE-2019-12437] Cross Site Request Forgery (CSRF) Protection Bypass in GraphQL (Aaron Carlino) - See [CVE-2019-12437](https://www.silverstripe.org/download/security-releases/cve-2019-12437)
* 2019-06-10 [7d32b4502](https://github.com/silverstripe/silverstripe-framework/commits/7d32b45028795bc1ad801039e065e821222e1e66) [CVE-2019-12246] Denial of Service on flush and development URL tools (Serge Latyntcev) - See [CVE-2019-12246](https://www.silverstripe.org/download/security-releases/cve-2019-12246)
* 2018-11-07 [74698af40](https://github.com/silverstripe/silverstripe-framework/commit/74698af402e0d8a4efe90d2db3591fb20b5ecf03) 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 [88d9131](https://github.com/silverstripe/silverstripe-graphql/commit/88d913118807ff4852dbe88b40e2de633647c9b0) CSRF protection (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007)
### API Changes
* 2019-05-06 [8ee50d2ba](https://github.com/silverstripe/silverstripe-framework/commit/8ee50d2ba7ff582bf317de7c1149d17bab1eb4fa) Remove DataObjectSchema::getFieldMap() (#8960) (Maxime Rainville)
* 2019-05-03 [5337e6d04](https://github.com/silverstripe/silverstripe-framework/commit/5337e6d04847c66d0a293e9c6c53a58d97aea3a1) Replace FormActions with anchors to enable panel-based loading in GridField navigation buttons (#8953) (Robbie Averill)
* 2019-04-30 [d325b8a](https://github.com/silverstripe/silverstripe-assets/commit/d325b8a6e0594ba50de1033edc5b99428ed6e5dd) Mark the FlysystemAssetStore FileResolutionStrategy getters and setters as internal (#255) (Maxime Rainville)
* 2019-04-16 [3c6357d](https://github.com/silverstripe/silverstripe-assets/commit/3c6357d5856857fe21d893495071d989a8500bed) Add an extension to the regular AssetStore interface (Maxime Rainville)
* 2019-04-12 [c3739d3](https://github.com/silverstripe/silverstripe-assets/commit/c3739d31c5a86a86d13b961c635b93cf34dec6a0) Allow FileIDHelper::build() to accept a ParsedFiledID (Maxime Rainville)
* 2019-04-10 [27f6165](https://github.com/silverstripe/silverstripe-assets/commit/27f6165afab041b69881133c4d206c885d2f90bb) Rename ParseFileID, add generateVariantFileID, add stripVariant on FileResolutionStrategy (Maxime Rainville)
* 2019-04-09 [ab01ac99](https://github.com/silverstripe/silverstripe-cms/commit/ab01ac99e3669db772f9e5a0a561aef6ba55b971) Deprecated CMSMain-&gt;publishall() (Ingo Schommer)
* 2019-04-09 [7be48e8](https://github.com/silverstripe/silverstripe-assets/commit/7be48e8792529fd4b23f92c6ec15fe5a0d67b531) Add a resolveFileID method to FileResolutionStrategy (Maxime Rainville)
* 2019-03-28 [77fc163](https://github.com/silverstripe/silverstripe-assets/commit/77fc163d33555dfb2477a790f1726a0aecf3f892) Add immutable setters to ParsedFileID (Maxime Rainville)
* 2019-03-26 [3f38c77](https://github.com/silverstripe/silverstripe-assets/commit/3f38c779ca43d51432c0ecd6ac6c4aac42f00d50) Add logic to find variants based on the FileID scheme. (Maxime Rainville)
* 2019-03-21 [e919291](https://github.com/silverstripe/silverstripe-assets/commit/e919291d2217421872bff9700f9d037b585490ee) Define a new FileResolutionStrategy API. (Maxime Rainville)
* 2019-03-20 [c123b64](https://github.com/silverstripe/silverstripe-assets/commit/c123b648f10845ba16c9c2d6bd836c2b286a86e2) Deprecate parseFileID, getFileID, getOriginalFilename and getVariant on FlysystemAssetStore (Maxime Rainville)
* 2019-03-20 [19e51a3](https://github.com/silverstripe/silverstripe-assets/commit/19e51a39b704d75835585bd3b3ce01c149c5003f) Add a LegacyPathFileIDHelperTest (Maxime Rainville)
* 2019-03-19 [6b450395c](https://github.com/silverstripe/silverstripe-framework/commit/6b450395cebdb5d8c734c10f365b5850f4038345) Allow empty arraylists to be typed (#8866) (Damian Mooyman)
* 2019-03-14 [e708e58](https://github.com/silverstripe/silverstripe-assets/commit/e708e5860cf60bd2c369fd9a37876d674605d48b) Move FileID parsing logic to dedicated helper class (Maxime Rainville)
* 2018-11-13 [580214cc3](https://github.com/silverstripe/silverstripe-framework/commit/580214cc30785906b012d3cb6e5bea67f3b5ff34) Add PHP deprecation notices to setLogger and getLogger (Robbie Averill)
* 2018-11-05 [ebfab45e2](https://github.com/silverstripe/silverstripe-framework/commit/ebfab45e23c1bc1dcfcb17f74d761f6c39251256) LoginForm::authentiator_class is now deprecated, use getters or setters instead (Robbie Averill)
* 2018-10-31 [0703c1a94](https://github.com/silverstripe/silverstripe-framework/commit/0703c1a94ed4e22ff9af2de1037693513f4313aa) Deprecating Permission::$declared_permissions and related methods/props (Maxime Rainville)
* 2018-10-28 [9724d1dd7](https://github.com/silverstripe/silverstripe-framework/commit/9724d1dd73fb1ca907c96d6ced2573bc7525d94c) Convert JSON methods are now deprecated, use json_encode or decode instead (Robbie Averill)
* 2018-10-23 [2773e9c0](https://github.com/silverstripe/silverstripe-cms/commit/2773e9c0752b64f1f29f8846661e74711a87d778) Deprecate CMSPageHistoryController (#2298) (Maxime Rainville)
* 2018-07-03 [a8853504b](https://github.com/silverstripe/silverstripe-framework/commit/a8853504b4be9288fcbfdc71c5949fc7c612c599) API MonologErrorHandler::setLogger is deprecated, use MonologErrorHandler::pushLogger instead (Robbie Averill)
### Features and Enhancements
* 2019-05-02 [1f78e8ae8](https://github.com/silverstripe/silverstripe-framework/commit/1f78e8ae80e9c7b3038c0c5a18c90bed467eec36) Clean up secureassets module artefacts (#8948) (Ingo Schommer)
* 2019-05-02 [236094c](https://github.com/silverstripe/silverstripe-assets/commit/236094ca379f78e7a97845c367d187c42c89b9f6) FixFilePermissionsTask to fix secureassets permissions (#250) (Andre Kiste)
* 2019-05-02 [48db515fb](https://github.com/silverstripe/silverstripe-framework/commit/48db515fbd892ef3a2464a2c364328ba752ca4c3) Fix folder permissions (#8950) (Andre Kiste)
* 2019-05-01 [0696045e5](https://github.com/silverstripe/silverstripe-framework/commit/0696045e59fa2e62b5021fe1045763c433fb13aa) Legacy thumbnail migration task (#8924) (Ingo Schommer)
* 2019-05-01 [30e7fe2](https://github.com/silverstripe/silverstripe-assets/commit/30e7fe26c3a485f0dfa48cb647b495efbf9f57c2) Migrate legacy thumbnails (fixes #235) (#242) (Ingo Schommer)
* 2019-04-30 [06f84d2](https://github.com/silverstripe/silverstripe-assets/commit/06f84d2e456137956d03e56265f072070fb61ed0) Clean up secureassets module artefacts (fixes #231) (Ingo Schommer)
* 2019-04-24 [43bde65](https://github.com/silverstripe/silverstripe-assets/commit/43bde657b73fae60b6eaec1d107e815f1f7247a3) Update FileMigrationHelper to normalise existing files (Maxime Rainville)
* 2019-04-24 [cfa36b7](https://github.com/silverstripe/silverstripe-asset-admin/commit/cfa36b7c61b79b81a035e1c2845e58340cd03b2e) improve visibility of insert file button (#934) (Aaron Carlino)
* 2019-04-23 [bb0ae72](https://github.com/silverstripe/silverstripe-assets/commit/bb0ae723c38b4115ef0c4b94845d01d0dbfe4439) Use natural paths for public files to support permalinks (#223) (Maxime Rainville)
* 2019-04-18 [80ad336e9](https://github.com/silverstripe/silverstripe-framework/commit/80ad336e970c102415d350e92283af4de48bfcf9) Add API to create a generator from a DataList (#8931) (Guy Marriott)
* 2019-04-15 [b1339f0d7](https://github.com/silverstripe/silverstripe-framework/commit/b1339f0d724396a39c1b5b1b7ae18239f63c812c) Update FieldList::replaceField API to match removeByName (#8876) (Guy Marriott)
* 2019-03-25 [ca6a343](https://github.com/silverstripe/silverstripe-graphql/commit/ca6a3438b5585a4257e2c458cb634212c8201a98) Add search params, filtering service for queries (#220) (Aaron Carlino)
* 2019-03-11 [25f1b17](https://github.com/silverstripe/silverstripe-graphql/commit/25f1b17975fa9d11c2c9811294a3e0b018476cf9) Operation descriptions (#210) (Ingo Schommer)
* 2019-03-05 [39a29fa2f](https://github.com/silverstripe/silverstripe-framework/commit/39a29fa2f65ebd9a3ad486c6d85249322bcb9a64) has_extension() should allow injector overrides (Aaron Carlino)
* 2019-02-22 [12512e84](https://github.com/silverstripe/silverstripe-cms/commit/12512e84b1765f144247d66e8d96b7d8f44e6015) BrokenLinksReport now uses injector for fields, uses short array syntax and single quotes (Robbie Averill)
* 2019-02-03 [8267623](https://github.com/silverstripe/silverstripe-admin/commit/8267623bc5044e47026d7716e859d2208b0e651e) Add getter for ModelAdmin::$modelClass (jcarter)
* 2019-02-01 [bbace74](https://github.com/silverstripe/silverstripe-campaign-admin/commit/bbace7455dc0ea4c21ee5128b0cf185580c05b58) Add to Campaign button in SiteTree now lives in campaign-admin (Robbie Averill)
* 2019-01-29 [c4bf06f60](https://github.com/silverstripe/silverstripe-framework/commit/c4bf06f6008e4619fb88075e6dc779d7acf595fd) Add new execmetric debug URL parameter to print out exection time and peak memory usage (Maxime Rainville)
* 2019-01-23 [13b8475](https://github.com/silverstripe/silverstripe-asset-admin/commit/13b847501f117a6286cf079550f7c0031c4edf14) add a memory limit to the ImageThumbnailHelper (Maxime Rainville)
* 2019-01-16 [6689db1b](https://github.com/silverstripe/silverstripe-cms/commit/6689db1ba983a91f7fb1b50644b84513f0381bef) Convert drag handle and dropdown caret to use font-icons in site tree (Sacha Judd)
* 2019-01-16 [e665820](https://github.com/silverstripe/silverstripe-admin/commit/e66582031e9b8c962beb8b1760324dd7b5588567) Convert drag handle and dropdown caret to use font-icons in site tree (Sacha Judd)
* 2019-01-14 [e0dc7ad](https://github.com/silverstripe/silverstripe-errorpage/commit/e0dc7ade43547e2a45ebc8bcd476496ce1346789) Add font-icon for site tree error page (Sacha Judd)
* 2019-01-14 [1f1f4496](https://github.com/silverstripe/silverstripe-cms/commit/1f1f44969a073635c4b236fa7d614466e34efebb) Add font-icon support for site tree (Sacha Judd)
* 2019-01-14 [17ff5cf](https://github.com/silverstripe/silverstripe-admin/commit/17ff5cf8eddc97a719833887fd38075f97d5bdcb) Add font-icon support for site tree (Sacha Judd)
* 2019-01-09 [1e01deea3](https://github.com/silverstripe/silverstripe-framework/commit/1e01deea39f54ef69cadbad8fc6d8a211fcb5ba4) Make resources dir configurable (#8519) (Maxime Rainville)
* 2019-01-07 [394dd4765](https://github.com/silverstripe/silverstripe-framework/commit/394dd4765c5d08f574a737bbd7ece1b2cb5258de) Scaffolded field labels now only have an uppercased first word (Robbie Averill)
* 2019-01-01 [a302acf](https://github.com/silverstripe/silverstripe-installer/commit/a302acfa5aaa8e55864b3c056c6939d1cf0899a0) Add Roave Security advisories to composer (Simon Erkelens)
* 2018-11-23 [dbb24f9](https://github.com/silverstripe/silverstripe-graphql/commit/dbb24f9d394b3f427912960c3d12cce45e55c47f) Persist query support (#179) (Aaron Carlino)
* 2018-11-20 [52a23441](https://github.com/silverstripe/silverstripe-reports/commit/52a234410d9337b812e6b1ef615850c0b3df3f7a) Extracting out the method to determine parameters (filters) for update the report sourceRecords (Guy Marriott)
* 2018-11-18 [d6b1c071](https://github.com/silverstripe/silverstripe-reports/commit/d6b1c071b6c04d44c13ac1737f406495381caec1) Adding tests for new report breadcrumbs feature (Guy Marriott)
* 2018-11-18 [cc712892a](https://github.com/silverstripe/silverstripe-framework/commit/cc712892a95b74c8bcdea5f2c1469286e298203e) Port betterbuttons to framework (#8569) (Andre Kiste)
* 2018-11-16 [edecbabe](https://github.com/silverstripe/silverstripe-reports/commit/edecbabe610bafd76045da3652dfa498e16dfb67) Allow reports to specify breadcrumbs for child reports (Guy Marriott)
* 2018-11-12 [acf4b3a](https://github.com/silverstripe/silverstripe-asset-admin/commit/acf4b3aeaed27aa301818189ddc943977218d599) MoveFormFactory::getForm is now extensible and no longer uses divider lines (Robbie Averill)
* 2018-11-09 [0f2eebe5d](https://github.com/silverstripe/silverstripe-framework/commit/0f2eebe5d41698e3d8de74e4b2cf38ea89bf7d1e) Change to variadic calls in ListDecorator and add unit tests (Robbie Averill)
* 2018-11-01 [2ff7ee675](https://github.com/silverstripe/silverstripe-framework/commit/2ff7ee6752cc505fd538a12e1a0c1709231961a8) Deprecate RandomGenerator::generateEntropy in favour of using random_bytes directly (Guy Marriott)
* 2018-10-20 [c418ee291](https://github.com/silverstripe/silverstripe-framework/commit/c418ee2915634ba4277688589a1ecf1d89fa6fba) Add getters and setters for public properties in ConfirmPasswordField, add tests (Robbie Averill)
* 2018-10-20 [3cdb73bd4](https://github.com/silverstripe/silverstripe-framework/commit/3cdb73bd44376161c79e0ccd38394f52203458c7) Add getLogger() to MonologErrorHandler and add test for exception without one (Robbie Averill)
* 2018-09-25 [12907271](https://github.com/silverstripe/silverstripe-cms/commit/12907271ffd230d787c597fb0413262a7f2d49c2) Add update extension hooks for LinkFormFactory subclasses (Robbie Averill)
* 2018-09-25 [4415655](https://github.com/silverstripe/silverstripe-admin/commit/44156558487117cb07f0312de3aa399149adfbbf) Add update extension hooks for LinkFormFactory subclasses (Robbie Averill)
* 2018-09-13 [0a64b07b2](https://github.com/silverstripe/silverstripe-framework/commit/0a64b07b2c25b39ec89dbbe1f8aaa213e9184386) Use Bootstrap alerts throughout the CMS (Robbie Averill)
* 2018-09-13 [05486897](https://github.com/silverstripe/silverstripe-siteconfig/commit/05486897cedf152ed159a37ff751178b8dbcec02) Use Bootstrap alerts throughout the CMS (Robbie Averill)
* 2018-09-13 [2fe58f8a](https://github.com/silverstripe/silverstripe-cms/commit/2fe58f8a064438290c0aed6f8e4a301e0fa3939a) Use Bootstrap alerts throughout the CMS (Robbie Averill)
* 2018-09-13 [f11cd44](https://github.com/silverstripe/silverstripe-admin/commit/f11cd44d21b0482b5fe3ee165d234a1ac19edfff) Use Bootstrap alerts throughout the CMS (Robbie Averill)
* 2018-07-15 [e20be929](https://github.com/silverstripe/silverstripe-cms/commit/e20be9293fe9093c510ea7c7acce8c518e123281) Meta tag components (Jonathon Menz)
* 2018-07-03 [1cb23178e](https://github.com/silverstripe/silverstripe-framework/commit/1cb23178ecaecff89e754e0298fd5b3ee1dfbdce) Separate core error logging from standard LoggerInterface (Robbie Averill)
* 2018-07-03 [d37551de3](https://github.com/silverstripe/silverstripe-framework/commit/d37551de34ee7b82cb4cc4dc04775387c0af64b3) Setters in DebugViewFreindlyErrorFormatter are now chainable (Robbie Averill)
### Bugfixes
* 2019-05-06 [856e84195](https://github.com/silverstripe/silverstripe-framework/commit/856e841955fa2f31b21ffddc34c373fab89a210a) Ensuring pagination buttons have a consistent state to work off of (#8957) (Guy Marriott)
* 2019-05-03 [768bee1](https://github.com/silverstripe/silverstripe-assets/commit/768bee122cd135bacacc31b33a466fd4df9b5898) Fix linting (Maxime Rainville)
* 2019-05-03 [2a91b777c](https://github.com/silverstripe/silverstripe-framework/commit/2a91b777c60d36ff227f3c2a9c5f7e1171de97be) Rewrite deprecation notice for declared_permissions (Maxime Rainville)
* 2019-05-03 [65b9465](https://github.com/silverstripe/silverstripe-assets/commit/65b94652b8d3b29097cf818d90d47d9c31b1d56d) Remove duplicate FileMigrationHelper class by aliaising it to the proper one (Maxime Rainville)
* 2019-05-03 [7cfa7716](https://github.com/silverstripe/silverstripe-cms/commit/7cfa771681080dceef0ed52065bfa84e5c5eb00b) Use Bootstrap 4 alert for page type restriction message when adding a page (Robbie Averill)
* 2019-05-02 [7ec9937](https://github.com/silverstripe/silverstripe-assets/commit/7ec993701ce4ad6f6ee60d1a32d20d09cc03f89a) Fix broken test (Maxime Rainville)
* 2019-05-02 [100a298](https://github.com/silverstripe/silverstripe-assets/commit/100a29826958a311a378b74b85a6f0354eca6e6b) Fix invalic file variant (bergice)
* 2019-05-02 [5fa823a](https://github.com/silverstripe/silverstripe-assets/commit/5fa823acfbd92f40547f539baada182ed4105145) Fix more tests (bergice)
* 2019-05-02 [8f5fa41](https://github.com/silverstripe/silverstripe-assets/commit/8f5fa41a47161145fa8e21c392dc2ef371d788ec) Fix tests, linting (Aaron Carlino)
* 2019-05-02 [0c3a7de](https://github.com/silverstripe/silverstripe-assets/commit/0c3a7de2fb9b9330d1f37375df010d6909e91724) Fix tests (bergice)
* 2019-05-01 [ecfe039e7](https://github.com/silverstripe/silverstripe-framework/commit/ecfe039e725f8bc15f244d241f1d42de54b35eac) Don't add "better buttons" previous and next without a paginator (Guy Marriott)
* 2019-05-01 [b335c68](https://github.com/silverstripe/silverstripe-assets/commit/b335c68e9c3c12a3f5466f57e92558a1d302cd0a) Split the new content unit test. (Maxime Rainville)
* 2019-04-30 [efaaa86](https://github.com/silverstripe/silverstripe-assets/commit/efaaa868af9746b018d0ed294d39d8d09372c8ba) Add more complicated tests for the TagsToShortcodeHelper (Maxime Rainville)
* 2019-04-29 [87db65f](https://github.com/silverstripe/silverstripe-assets/commit/87db65f9e4080766ef19c65173d0b7044ec79279) Set correct COMPOSER_ROOT_VERSION value (Maxime Rainville)
* 2019-04-29 [5d237b0](https://github.com/silverstripe/silverstripe-versioned/commit/5d237b019c1b189c201d74819baf5e05db1de79b) Fix getQueryParam() on null error (Sheila Bañez)
* 2019-04-28 [71c72f0](https://github.com/silverstripe/silverstripe-assets/commit/71c72f0d9c6cac33a5facdac7e0552ab1f289a2d) Fix minor linting issue (Maxime Rainville)
* 2019-04-28 [31a9fcb](https://github.com/silverstripe/silverstripe-assets/commit/31a9fcb506d78412879b25a302ff74cbedb7f7e7) Ditch ExtendedAssetStore interface. (Maxime Rainville)
* 2019-04-26 [346b7e3](https://github.com/silverstripe/silverstripe-assets/commit/346b7e3e67436715bdecc3a11ca1135a3295956a) Tweak FileMigrationHelper to not skip files and make it a bit more performant (Maxime Rainville)
* 2019-04-26 [03d38f2](https://github.com/silverstripe/silverstripe-assets/commit/03d38f2a817f970b6e75cc6a44e784b0e2e9eae4) Unload the Intervention Image resource so it can be garbaged collected (Maxime Rainville)
* 2019-04-24 [3a86fa2](https://github.com/silverstripe/silverstripe-asset-admin/commit/3a86fa2b1c09f3da49a28718f7e37cee3d839450) Adjsut unit test to work with new natural paths (#932) (Maxime Rainville)
* 2019-04-23 [c0a8886](https://github.com/silverstripe/silverstripe-assets/commit/c0a88863951e11001922ec0a8346e7dc16b6d2d5) Adapt FileMigrationHelper to normalise location of files (Maxime Rainville)
* 2019-04-23 [04c1bbf](https://github.com/silverstripe/silverstripe-assets/commit/04c1bbf3f51759f16f0d6d75f0657aa2bea04037) Get current Migration task working with permalink (Maxime Rainville)
* 2019-04-22 [fe4d7c4](https://github.com/silverstripe/silverstripe-assets/commit/fe4d7c4d60378c5fdbdf4b6543e7274016e08fe1) Make sure we don't override existing files when performing operations on a file and all its variants (Maxime Rainville)
* 2019-04-18 [c63c8b0](https://github.com/silverstripe/silverstripe-assets/commit/c63c8b06b9f1b18fe05adf5a84414073e816641b) Return a permanent redirect when a file has been published under its normal path (Maxime Rainville)
* 2019-04-18 [e6c1061](https://github.com/silverstripe/silverstripe-asset-admin/commit/e6c1061600941ffa26ec42fc4fc7032d894e944d) folders always go first when ordering (#936) (Serge Latyntsev)
* 2019-04-18 [353f2b5](https://github.com/silverstripe/silverstripe-assets/commit/353f2b56b1bf96cf8cf716298369dde77d37df30) Implement feedback from peer review (Maxime Rainville)
* 2019-04-18 [bfa7021](https://github.com/silverstripe/silverstripe-assets/commit/bfa7021f50a280b4dde7227aee5443350ed92f5c) Fix typos from code review (Maxime Rainville)
* 2019-04-17 [e1234a5](https://github.com/silverstripe/silverstripe-assets/commit/e1234a5e5d9521a37dc4ce37624564c6f4c9923f) Fix typo to fetch a dynamic field rather than always assume it's called content. (Maxime Rainville)
* 2019-04-17 [da1af3d8b](https://github.com/silverstripe/silverstripe-framework/commit/da1af3d8b01ac6928447593f055d6476bbbc03f4) Postgres booleans should return as int for consistency (Guy Marriott)
* 2019-04-16 [2e5467a](https://github.com/silverstripe/silverstripe-admin/commit/2e5467a609908ac6ea1b0841eeac4f80c5cb26dd) TinyMCE not updating form state (Aaron Carlino)
* 2019-04-16 [0d43492](https://github.com/silverstripe/silverstripe-assets/commit/0d43492bc08493ac1b310a4117185eb0119ef236) Add methods to normalise file path to confirm with the default file ID of the strategy. (Maxime Rainville)
* 2019-04-16 [9d6b5048a](https://github.com/silverstripe/silverstripe-framework/commit/9d6b5048a620f793a2910b858331a5141d161e63) Table aliases are retained on base tables in queries built using SQLConditionalExpression (#8918) (Guy Marriott)
* 2019-04-15 [63360f804](https://github.com/silverstripe/silverstripe-framework/commit/63360f80482d860495900a39282b54680f38cd45) Replace substr with mb_substr to get the correct position (Sheila Bañez)
* 2019-04-15 [4fbe0fd6](https://github.com/silverstripe/silverstripe-cms/commit/4fbe0fd6b9018d2ff16f5edc022daa92702e91a5) Fix linking anchor on the same page (#2388) (Will Rossiter)
* 2019-04-15 [0b56a563](https://github.com/silverstripe/silverstripe-cms/commit/0b56a563c0b6f51cc94589b12693c5326f0c878c) Fixes #2110 added default Title value for saved pages. (#2366) (ttunua)
* 2019-04-15 [4302fb1](https://github.com/silverstripe/silverstripe-assets/commit/4302fb15eac1eaecf73f599c4c1d0ce7d5fe877d) Tweak findVariants to not return null, because in 5.6 yield and return can not be used in the same method (Maxime Rainville)
* 2019-04-14 [a48beac84](https://github.com/silverstripe/silverstripe-framework/commit/a48beac84544ed2db89ac094cc80508742284c1c) Calculate threshold condition with SQL rather than PHP (Guy Marriott)
* 2019-04-13 [e561d06](https://github.com/silverstripe/silverstripe-assets/commit/e561d06e73724e8f517f6ee911b39f5b17531a8e) Tweak FileID helper to handle stack variant (Maxime Rainville)
* 2019-04-12 [ade7c9d](https://github.com/silverstripe/silverstripe-assets/commit/ade7c9d8d10fdc8038bef0b3d2c8b8a0932642d0) Add test to make sure we write the variants next to the main file (Maxime Rainville)
* 2019-04-12 [64e9560](https://github.com/silverstripe/silverstripe-assets/commit/64e9560097b8c87a19a9b2446526a6a65d31e062) Write some unit test for stripVariant and generateVariantFileID on FileIDHelperResolutionStrategy (Maxime Rainville)
* 2019-04-12 [23d55f1](https://github.com/silverstripe/silverstripe-assets/commit/23d55f12967774a0ccf3e1d50f34689714dc3fc5) Deprecate legacy filename usage and add extra unit tests (Maxime Rainville)
* 2019-04-12 [91ea306](https://github.com/silverstripe/silverstripe-assets/commit/91ea30687976fadd27582d2d90cf9f2ec7ca8f99) Tweak FileIDResolutionStrategy to better handle hashless tuple (Maxime Rainville)
* 2019-04-11 [bf1dbec](https://github.com/silverstripe/silverstripe-assets/commit/bf1dbec487cf51ec5fb6b656461466736894b1b3) Provide a strategy for legacu_filenames (Maxime Rainville)
* 2019-04-11 [24c72c1](https://github.com/silverstripe/silverstripe-assets/commit/24c72c1d05f49c3625400fafbbe25177ffbd419b) Add explicit test to make sure files are written to the expected store (Maxime Rainville)
* 2019-04-11 [0b6e5d3](https://github.com/silverstripe/silverstripe-assets/commit/0b6e5d3f30fd7678d0d58ab4c1c665ac9a79bf6b) Update setFromString and setFromLocalFile to wrap data around stream and call setFromStream (Maxime Rainville)
* 2019-04-11 [6baf400](https://github.com/silverstripe/silverstripe-assets/commit/6baf4008f90e38ece24bd3e730066d0e5e431c4b) Add some swapPublish logic to publish to store that don't support hash paths and add extra validation around hashes (Maxime Rainville)
* 2019-04-11 [07cc061](https://github.com/silverstripe/silverstripe-assets/commit/07cc061c1a8a1fb93a3003d78f7a2520793fe806) Add some extra logic to read the hash from the file content when it can't be picked up from the file id (Maxime Rainville)
* 2019-04-10 [4b0d5c8](https://github.com/silverstripe/silverstripe-assets/commit/4b0d5c80c192eab1c4114e203d41cfd2e532872a) #8916 Prevent session generation on file_link shortcode handling (micmania1)
* 2019-04-10 [cd5fdca](https://github.com/silverstripe/silverstripe-assets/commit/cd5fdca8c2fb1ff45f064535f28557a781bb6a37) FInish converting all the methods on FlysystemAssetStore (Maxime Rainville)
* 2019-04-10 [12ae61d](https://github.com/silverstripe/silverstripe-assets/commit/12ae61dd62c0abae3263792d8fa52dd45dc7360f) Tweak the NaturalFileIDHelper so it doesn't accept legacy style variant file ids (Maxime Rainville)
* 2019-04-09 [661a27e](https://github.com/silverstripe/silverstripe-assets/commit/661a27e93efcf98c2521b42ec802ecf625e0a6ea) Fix hash redirection logic on PostreSQL and add PostreSQL to the travis matrix (#237) (Serge Latyntsev)
* 2019-04-09 [9a4395238](https://github.com/silverstripe/silverstripe-framework/commit/9a439523851d16913eeda694c05cb4ccd69c1df9) Fix formatting (Al)
* 2019-04-09 [956b268](https://github.com/silverstripe/silverstripe-assets/commit/956b268b2f69eeed86e43c6e0f1e6e0840c846c0) Fix hash redirection logic on PostreSQL and add PostreSQL to the travis matrix (Maxime Rainville)
* 2019-04-09 [a61cb1de9](https://github.com/silverstripe/silverstripe-framework/commit/a61cb1de9952f9a8521ed7f382b38de59fd653b9) Fix reference to webconfig.php, an invalid file (Matt Peel)
* 2019-04-09 [7ca4ee5](https://github.com/silverstripe/silverstripe-assets/commit/7ca4ee5bd583e779897797473479345c32eb04f8) Fix unit tests (Maxime Rainville)
* 2019-04-08 [f12fa62ad](https://github.com/silverstripe/silverstripe-framework/commit/f12fa62ad60e643bb93cc191f77cc75404b6a25c) Better error message when GridFieldLevelup passed bad record details (Sam Minnee)
* 2019-04-05 [594af7713](https://github.com/silverstripe/silverstripe-framework/commit/594af7713487da0fc200d6df8d9c706c26e3c767) prevent unnecessary field alterations for enums with empty defaults (Loz Calver)
* 2019-04-05 [1cfc4c7](https://github.com/silverstripe/silverstripe-assets/commit/1cfc4c73686a14c36c619a236cf63db44beb3c8d) Still fixing unit tests (Maxime Rainville)
* 2019-04-05 [f0b61bd](https://github.com/silverstripe/silverstripe-assets/commit/f0b61bd29a7c1c22d2ce30f480b7478e3e4fe61e) Hard fail when trying to build a Hash file ID without prvoding a hash (Maxime Rainville)
* 2019-04-05 [4cdaae9](https://github.com/silverstripe/silverstripe-assets/commit/4cdaae93b2f5852764d9ec0da67736f0f59c43c8) Explicitely set hash when returning variant parsed ID (Maxime Rainville)
* 2019-04-04 [759968bbe](https://github.com/silverstripe/silverstripe-framework/commit/759968bbe2f8e3a4087b2f08622abc4cc70f2867) Fix Undefined variable: result when catch Exception (Ian Patel)
* 2019-04-04 [a3c61e5](https://github.com/silverstripe/silverstripe-admin/commit/a3c61e546f77e78fdefd3e678b0dbe0ca52e933b) Long site names now display correctly in CMS menu with equal margins and alignment (Robbie Averill)
* 2019-04-04 [b542585](https://github.com/silverstripe/silverstripe-assets/commit/b5425850b3c048bdeda66821ad9c889ad7c0d767) Convert more of FlyAssetStore to use new format (Maxime Rainville)
* 2019-04-04 [4be41a8](https://github.com/silverstripe/silverstripe-assets/commit/4be41a8fa8665c02b805b47dc11a00674453e441) Define and test a softResolveFileID method on FileResolutionStrategy (Maxime Rainville)
* 2019-04-04 [ad5d379](https://github.com/silverstripe/silverstripe-admin/commit/ad5d3796f2029f422e2a696f3a7d0a640d5eef36) Show RightTitle on CheckboxField in React forms (Sam Minnee)
* 2019-04-04 [a17e1de](https://github.com/silverstripe/silverstripe-admin/commit/a17e1deee08a4fc73470dfc1ac9083b7d019fbc8) Show RightTitle on CheckboxField (Sam Minnee)
* 2019-04-04 [8a098d637](https://github.com/silverstripe/silverstripe-framework/commit/8a098d637fd67ba20e98ec76d51195f74e8eb131) Show RightTitle on CheckboxField (Sam Minnee)
* 2019-04-03 [c767d81](https://github.com/silverstripe/silverstripe-assets/commit/c767d813af591e0f2bf99df0ff6d5d312edb1ab9) Adjust test to work with new asset structure (Maxime Rainville)
* 2019-04-03 [fbf385a](https://github.com/silverstripe/silverstripe-assets/commit/fbf385afcbb95f1d3c6fa2399d7d6e76c7daaa52) Adjust writting logic to work with file resolution strategies (Maxime Rainville)
* 2019-04-03 [ad828a4](https://github.com/silverstripe/silverstripe-assets/commit/ad828a458430f982ae8057b7e967d66073400169) Validate hash when looking for variant (Maxime Rainville)
* 2019-03-29 [c84ad4278](https://github.com/silverstripe/silverstripe-framework/commit/c84ad4278f924679ba8c0d258c806ddd610c6bc5) Update installer to create the assets folder if its missing (Maxime Rainville)
* 2019-03-28 [2b386e6](https://github.com/silverstripe/silverstripe-assets/commit/2b386e6e34aaeccd714b8f0f3f2c7de1b7f71b27) Fix the exist and delete logic when working (Maxime Rainville)
* 2019-03-26 [83ec0b69f](https://github.com/silverstripe/silverstripe-framework/commit/83ec0b69fa642ed1ad734fff10ea6dc3aeba6cf3) Resolve issue where schema changes between enum / non-enum types (Damian Mooyman)
* 2019-03-25 [fae19c16b](https://github.com/silverstripe/silverstripe-framework/commit/fae19c16b54f077bbd7665a50df516d290faa07e) has_one File form scaffolding (Jonathon Menz)
* 2019-03-22 [95344a6](https://github.com/silverstripe/silverstripe-asset-admin/commit/95344a6e291dc40c361ebbcb787eb9656b5aa905) Re-instate assuming redux knows best with more specific checks (See #922) (Guy Marriott)
* 2019-03-22 [cb670f4](https://github.com/silverstripe/silverstripe-admin/commit/cb670f4604dab8328b939ebe583f09928ecbe32d) TinyMCE editor.scss now applies to the correct body class, and has correct broken link colours (Robbie Averill)
* 2019-03-22 [db6e105](https://github.com/silverstripe/silverstripe-versioned/commit/db6e105f97f9a2b2d725c85dac75dfada6b10bf1) ReadVersions now uses public accessors for private dataObjectClass property (Robbie Averill)
* 2019-03-21 [595d8ec](https://github.com/silverstripe/silverstripe-asset-admin/commit/595d8ec8bc11e102ab6ef3ec1cbb99e9d5c09cab) UploadField now ensures that file data is copied to redux store of value updates (Guy Marriott)
* 2019-03-21 [e1190e33d](https://github.com/silverstripe/silverstripe-framework/commit/e1190e33d28df5109ed27ff12b6fd290e9eff1a5) Fix PDOConnector GeneratedID return type (Johannes Hammersen)
* 2019-03-20 [388baa01b](https://github.com/silverstripe/silverstripe-framework/commit/388baa01b49fb47b7187c03bfdaa87b6712f5cdd) Fix linting (Aaron Carlino)
* 2019-03-19 [aa491d929](https://github.com/silverstripe/silverstripe-framework/commit/aa491d92940282a62748bbfe4f69a1e56b0ce2d8) Fix tests (Aaron Carlino)
* 2019-03-18 [7f5ae1c](https://github.com/silverstripe/silverstripe-admin/commit/7f5ae1c7103d55642ca8201a60731357f91bd9bf) Increment bootstrap requirements (Maxime Rainville)
* 2019-03-14 [fd98212](https://github.com/silverstripe/silverstripe-versioned-admin/commit/fd98212390b084f742a98ad0f9bd0741d299213f) Bump bootstrap and merge js dependency (Maxime Rainville)
* 2019-03-13 [4d35ba3](https://github.com/silverstripe/silverstripe-campaign-admin/commit/4d35ba3076b7b7f5be0f735c910439336307f4e0) Bump JS dependencies for merge and bootstrap (Maxime Rainville)
* 2019-03-13 [c7b3b307](https://github.com/silverstripe/silverstripe-cms/commit/c7b3b3072829065b4b61674bd89973b78e6009c7) Bump JS dependencies for merge and bootstrap (Maxime Rainville)
* 2019-03-13 [11bcf3e](https://github.com/silverstripe/silverstripe-asset-admin/commit/11bcf3e24e0893756a4a82a02fc3a94f33357bf4) Bump JS depednencies for merge and bootstrap (Maxime Rainville)
* 2019-03-13 [a43593b](https://github.com/silverstripe/silverstripe-admin/commit/a43593b0ec5588b1126ee50925b55442eb4bdebb) Upgrade merge and bootstrap JS dependencies (Maxime Rainville)
* 2019-03-11 [ca781c684](https://github.com/silverstripe/silverstripe-framework/commit/ca781c684de6bbd675833151e418c9aa6eed5a67) RequestHandler::__construct() should run after middlewares (fixes #8848) (Loz Calver)
* 2019-02-26 [252397d8d](https://github.com/silverstripe/silverstripe-framework/commit/252397d8d1bd518af50b3064818642565783ec7e) Fix #8829: mention get_one does not escape field names (Nicola Fontana)
* 2019-02-26 [d1fa6e40d](https://github.com/silverstripe/silverstripe-framework/commit/d1fa6e40d8797a6f33ed513da4ca92985d0f24d1) Fix some minor typos (Andre Kiste)
* 2019-02-25 [49c05ce](https://github.com/silverstripe/silverstripe-admin/commit/49c05ce2e664fc981c8fe2bcc30a00f700dd5dfa) Update font-icon hover for site tree 'Add new page here' (Sacha Judd)
* 2019-02-25 [a0aaf050](https://github.com/silverstripe/silverstripe-cms/commit/a0aaf050d4c34417bcfb0ab4cc29e22d1c7191fb) Deprecate creatableChildren and add new function to support font-icon classes for allowedChildren (Sacha Judd)
* 2019-02-02 [1a7b23a2](https://github.com/silverstripe/silverstripe-cms/commit/1a7b23a21fd96329f689486088bb24681bcf295b) URL segment generation tests for resources dir are now accurate (Robbie Averill)
* 2019-02-01 [f9aeeb1d](https://github.com/silverstripe/silverstripe-cms/commit/f9aeeb1d6cd96e0de816a48f48ad2b32a76f488b) Remove coupling from SiteTree to campaign admin module (Robbie Averill)
* 2019-01-31 [7c5b73881](https://github.com/silverstripe/silverstripe-framework/commit/7c5b73881b3621ed4f59ab28e0efd7ea41ce6bcc) Prevent null-&gt;null being flagged as a value change (fixes #8774) (Loz Calver)
* 2019-01-11 [bbffe055](https://github.com/silverstripe/silverstripe-cms/commit/bbffe05541e14cac099d58e0c4146d9fbfefdb01) Fixing linting error. (Maxime Rainville)
* 2019-01-10 [f05afac](https://github.com/silverstripe/silverstripe-asset-admin/commit/f05aface2106e34cfd0ca126df690705994b5aac) Fix case difference in form field label assertion failure (Robbie Averill)
* 2019-01-08 [50837b4](https://github.com/silverstripe/silverstripe-graphql/commit/50837b44a5611d39c560e63f5eaf73755051aaf2) Fix duplicated class import declarations from merge up (Robbie Averill)
* 2018-12-22 [33854ce](https://github.com/silverstripe/silverstripe-graphql/commit/33854ce26ba0d05d5893934f53e111caa5d5fe08) do not pass SourceLocation to createLocatedError (Nicola Fontana)
* 2018-12-18 [0397c54b5](https://github.com/silverstripe/silverstripe-framework/commit/0397c54b5a2add7117d1f56618af8a1cb086adf7) Fixes #8459 (Russell Michell)
* 2018-12-10 [3d403f2](https://github.com/silverstripe/silverstripe-graphql/commit/3d403f246bb760ad06f4644d19e4e006c0e37637) Ensure httpMethod context is applied to all controller actions (#194) (Aaron Carlino)
* 2018-12-02 [0692a30](https://github.com/silverstripe/silverstripe-admin/commit/0692a30ee3bfc68f9d01764ddedeb6d22039bb88) PopoverOptionSet now explicitly sets an auto height for its search field (Robbie Averill)
* 2018-11-30 [cc7aa7b68](https://github.com/silverstripe/silverstripe-framework/commit/cc7aa7b68bf7c1ee739279d2a869f884955d5d0d) incorrect composer module type (Ed Linklater)
* 2018-11-30 [0c17ffc94](https://github.com/silverstripe/silverstripe-framework/commit/0c17ffc944a156937732238c9d8b6f1e0049e5ea) Manifest should ignore vendor folders within packages contained in vendor (Sam Minnee)
* 2018-11-26 [f22a4b980](https://github.com/silverstripe/silverstripe-framework/commit/f22a4b980bff59a425c3bd30d0af4bed2b77ff2f) getComponentByType can return null - prevent null pointer errors (Robbie Averill)
* 2018-11-26 [efa427fc4](https://github.com/silverstripe/silverstripe-framework/commit/efa427fc452fac132f50c7bc620dcbbc53f66c5a) Remove redundant "rightGroup" logic and increase getRightGroupField to protected (Robbie Averill)
* 2018-11-21 [b8796be](https://github.com/silverstripe/silverstripe-admin/commit/b8796be38dfbf9c5eaafc68fb2f674c74847f810) Downgrade to @storybook/addon-notes to 3.4 to allow pattern lib to build (Maxime Rainville)
* 2018-11-16 [c9c7c0c82](https://github.com/silverstripe/silverstripe-framework/commit/c9c7c0c8253de37b794a9427790dfb268125a0df) Fix PDO cached statement column coercion (Sam Minnee)
* 2018-11-13 [8854f053c](https://github.com/silverstripe/silverstripe-framework/commit/8854f053c80ca16b75fa5d97406b83f60c1e520a) Fix rebase conflicts (Robbie Averill)
* 2018-11-11 [45e1fcaf3](https://github.com/silverstripe/silverstripe-framework/commit/45e1fcaf309bc3d3a6ae9da9a716cf4ff935b1ad) Correct type coercion of MySQL (Sam Minnee)
* 2018-11-11 [adb6e9eb8](https://github.com/silverstripe/silverstripe-framework/commit/adb6e9eb8df6787e3de0f637e9f9bfab8974acf2) Perform type coercion on PDO-based MySQL and SQLite connections (Sam Minnee)
* 2018-11-09 [a8d3b9517](https://github.com/silverstripe/silverstripe-framework/commit/a8d3b95175c8336999e617e3334551ada5a49919) Make test work with utf8mb4 (Sam Minnee)
* 2018-11-05 [7775f8258](https://github.com/silverstripe/silverstripe-framework/commit/7775f82584236a441262e1ec6164556567c00cbd) Handle falsy return value when setting form field value in setAuthenticatorClass() (Robbie Averill)
* 2018-11-01 [7086f2ea3](https://github.com/silverstripe/silverstripe-framework/commit/7086f2ea3a86184c0d6aa3eb3ebe04fe3a9a9164) many many through not sorting by join table (#8534) (Michael Strong)
* 2018-10-30 [ba9ccb0](https://github.com/silverstripe/silverstripe-admin/commit/ba9ccb09f71c70978c8944ae2d76d1c27d7dda82) Update gridfield sorted icon and border colours (Sacha Judd)
* 2018-10-30 [d8f9162](https://github.com/silverstripe/silverstripe-admin/commit/d8f91626c5b19e4a3e8811f16b65be14b33cb2d0) Remove incorrect modal close icon hover colour (Sacha Judd)
* 2018-10-29 [1c6e22239](https://github.com/silverstripe/silverstripe-framework/commit/1c6e222391d37f879c8575c94d125e99780cbbef) Fix the GitHub issue template (Serge Latyntcev)
* 2018-10-28 [3425005](https://github.com/silverstripe/silverstripe-versioned-admin/commit/3425005429642c79525ab2bb750d18a7489a2832) Replace usage of Convert JSON methods with json_encode and json_decode (Robbie Averill)
* 2018-10-28 [ab739c7f](https://github.com/silverstripe/silverstripe-cms/commit/ab739c7fb011d2a9a49c7ddb88bb46821484e985) Replace usage of Convert JSON methods with json_encode and json_decode (Robbie Averill)
* 2018-10-28 [87ee897](https://github.com/silverstripe/silverstripe-campaign-admin/commit/87ee897fe794bf53f604ff8e21540e630c523e06) Replace usage of Convert JSON methods with json_encode and json_decode (Robbie Averill)
* 2018-10-28 [4a06f52](https://github.com/silverstripe/silverstripe-asset-admin/commit/4a06f524c7901949cc3e2ad2ae63b536f793a286) Replace usage of Convert JSON methods with json_encode and json_decode (Robbie Averill)
* 2018-10-28 [89c5abe](https://github.com/silverstripe/silverstripe-admin/commit/89c5abe834f61f2aa2a8c30ddfdb585e5da44a51) Replace usage of Convert JSON methods with json_encode and json_decode (Robbie Averill)
* 2018-10-28 [b02a6fa02](https://github.com/silverstripe/silverstripe-framework/commit/b02a6fa02db367eca9951da11c9073df35076b84) Replace usage of Convert JSON methods with json_encode (Robbie Averill)
* 2018-10-25 [bed1906f7](https://github.com/silverstripe/silverstripe-framework/commit/bed1906f7390ee9ebb0ad1cc581e4acd667184f9) Fix typo (Andre Kiste)
* 2018-10-24 [f635a2d](https://github.com/silverstripe/silverstripe-admin/commit/f635a2dacb69f84183d57dd742ff794c00005210) Fix typo (bergice)
* 2018-10-20 [c06cf4820](https://github.com/silverstripe/silverstripe-framework/commit/c06cf4820e02a923a683419b03979bd3677f38db) Readonly and disabled CurrencyFields no longer always returns dollar currency sign, now respect config (Robbie Averill)
* 2018-10-18 [76255c9fb](https://github.com/silverstripe/silverstripe-framework/commit/76255c9fb599d012dbd9498c98beb25b0f5d7f74) CheckboxSetField can now save into DBMultiEnum (Sam Minnee)
* 2018-10-18 [5531baa87](https://github.com/silverstripe/silverstripe-framework/commit/5531baa87f2cbfae76b5733016f860917813eea5) Introduce readonly transaction test to all database. (Sam Minnee)
* 2018-10-18 [1e83dff4e](https://github.com/silverstripe/silverstripe-framework/commit/1e83dff4edf19b24e02400589be4b4af41e25ac1) #828 optimised query in graphql asset admin (micmania1)
* 2018-10-16 [b4201fcf7](https://github.com/silverstripe/silverstripe-framework/commit/b4201fcf74d5472f39a84ad24d1ee9fdccf7cf16) Fix example code (DorsetDigital)
* 2018-10-15 [d4d9cbf](https://github.com/silverstripe/silverstripe-assets/commit/d4d9cbfbc51421ce7f6a4dff984281891c89d0b0) allow base path of / (Sam Minnee)
* 2018-10-08 [bd5a81590](https://github.com/silverstripe/silverstripe-framework/commit/bd5a815909d735c8223a552d8e79ebc13481a3aa) Make all enums non-destructive, not just ClassName (Sam Minnee)
* 2018-10-08 [67fe41d00](https://github.com/silverstripe/silverstripe-framework/commit/67fe41d00b3f58358a4bed7b8925bd21dc88726e) Ensure that repeated setting/unsetting doesnt corrode forceChange() (Sam Minnee)
* 2018-10-04 [5bb2d9484](https://github.com/silverstripe/silverstripe-framework/commit/5bb2d9484a203345964f75e6480bbfd5388aa824) Update “original” DataObject data to be the content of the last write (Sam Minnee)
* 2018-10-04 [a7b5de5de](https://github.com/silverstripe/silverstripe-framework/commit/a7b5de5de47319b204324d23e5a33403f28df5f7) ensure that there are PGSQL builds both with and without PDO (Sam Minnee)
* 2018-10-04 [261539953](https://github.com/silverstripe/silverstripe-framework/commit/261539953568e18361d30d7603c22ad3cb7c8cef) Use PDOs built-in transaction support in MySQLDatabase. (Sam Minnee)
* 2018-10-04 [0111b98b1](https://github.com/silverstripe/silverstripe-framework/commit/0111b98b18a273ca5c37013d880c5d21f94396af) Ensure that types are preserved fetching from database (Sam Minnee)
* 2018-09-14 [274657f4f](https://github.com/silverstripe/silverstripe-framework/commit/274657f4f815cfb990c23b39ab81c1def91b37ad) Add support in "I should see a message" step definition for Bootstrap alerts (Robbie Averill)
* 2018-07-04 [18293f7af](https://github.com/silverstripe/silverstripe-framework/commit/18293f7afed8db2c4a83356312027d8245fd4c3f) Rename pushHandler to pushLogger (Robbie Averill)

View File

@ -375,6 +375,33 @@ class Director implements TemplateGlobalProvider
return $response; return $response;
} }
/**
* Returns indication whether the manifest cache has been flushed
* in the beginning of the current request.
*
* That could mean the current active request has `?flush` parameter.
* Another possibility is a race condition when the current request
* hits the server in between another request `?flush` authorisation
* and a redirect to the actual flush.
*
* @return bool
*
* @deprecated 5.0 Kernel::isFlushed to be used instead
*/
public static function isManifestFlushed()
{
$kernel = Injector::inst()->get(Kernel::class);
// Only CoreKernel implements this method at the moment
// Introducing it to the Kernel interface is a breaking change
if (method_exists($kernel, 'isFlushed')) {
return $kernel->isFlushed();
}
$classManifest = $kernel->getClassLoader()->getManifest();
return $classManifest->isFlushed();
}
/** /**
* Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
* object to return, then this will return the current controller. * object to return, then this will return the current controller.
@ -1018,7 +1045,7 @@ class Director implements TemplateGlobalProvider
*/ */
public static function is_cli() public static function is_cli()
{ {
return in_array(php_sapi_name(), ['cli', 'phpdbg']); return Environment::isCli();
} }
/** /**
@ -1034,6 +1061,33 @@ class Director implements TemplateGlobalProvider
return $kernel->getEnvironment(); return $kernel->getEnvironment();
} }
/**
* Returns the session environment override
*
* @internal This method is not a part of public API and will be deleted without a deprecation warning
*
* @param HTTPRequest $request
*
* @return string|null null if not overridden, otherwise the actual value
*/
public static function get_session_environment_type(HTTPRequest $request = null)
{
$request = static::currentRequest($request);
if (!$request) {
return null;
}
$session = $request->getSession();
if (!empty($session->get('isDev'))) {
return Kernel::DEV;
} elseif (!empty($session->get('isTest'))) {
return Kernel::TEST;
}
}
/** /**
* This function will return true if the site is in a live environment. For information about * This function will return true if the site is in a live environment. For information about
* environment types, see {@link Director::set_environment_type()}. * environment types, see {@link Director::set_environment_type()}.

View File

@ -4,7 +4,14 @@ namespace SilverStripe\Control;
use SilverStripe\Control\Middleware\HTTPMiddlewareAware; use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
use SilverStripe\Core\Application; use SilverStripe\Core\Application;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Kernel; use SilverStripe\Core\Kernel;
use SilverStripe\Core\Startup\FlushDiscoverer;
use SilverStripe\Core\Startup\CompositeFlushDiscoverer;
use SilverStripe\Core\Startup\CallbackFlushDiscoverer;
use SilverStripe\Core\Startup\RequestFlushDiscoverer;
use SilverStripe\Core\Startup\ScheduledFlushDiscoverer;
use SilverStripe\Core\Startup\DeployFlushDiscoverer;
/** /**
* Invokes the HTTP application within an ErrorControlChain * Invokes the HTTP application within an ErrorControlChain
@ -18,11 +25,73 @@ class HTTPApplication implements Application
*/ */
protected $kernel; protected $kernel;
/**
* A custom FlushDiscoverer to be kept here
*
* @var FlushDiscoverer
*/
private $flushDiscoverer = null;
/**
* Initialize the application with a kernel instance
*
* @param Kernel $kernel
*/
public function __construct(Kernel $kernel) public function __construct(Kernel $kernel)
{ {
$this->kernel = $kernel; $this->kernel = $kernel;
} }
/**
* Override the default flush discovery
*
* @param FlushDiscoverer $discoverer
*
* @return $this
*/
public function setFlushDiscoverer(FlushDiscoverer $discoverer)
{
$this->flushDiscoverer = $discoverer;
return $this;
}
/**
* Returns the current flush discoverer
*
* @param HTTPRequest $request a request to probe for flush parameters
*
* @return FlushDiscoverer
*/
public function getFlushDiscoverer(HTTPRequest $request)
{
if ($this->flushDiscoverer) {
return $this->flushDiscoverer;
}
return new CompositeFlushDiscoverer([
new ScheduledFlushDiscoverer($this->kernel),
new DeployFlushDiscoverer($this->kernel),
new RequestFlushDiscoverer($request, $this->getEnvironmentType())
]);
}
/**
* Return the current environment type (dev, test or live)
* Only checks Kernel and Server ENV as we
* don't have sessions initialized yet
*
* @return string
*/
protected function getEnvironmentType()
{
$kernel_env = $this->kernel->getEnvironment();
$server_env = Environment::getEnv('SS_ENVIRONMENT_TYPE');
$env = !is_null($kernel_env) ? $kernel_env : $server_env;
return $env;
}
/** /**
* Get the kernel for this application * Get the kernel for this application
* *
@ -41,10 +110,10 @@ class HTTPApplication implements Application
*/ */
public function handle(HTTPRequest $request) public function handle(HTTPRequest $request)
{ {
$flush = array_key_exists('flush', $request->getVars()) || ($request->getURL() === 'dev/build'); $flush = (bool) $this->getFlushDiscoverer($request)->shouldFlush();
// Ensure boot is invoked // Ensure boot is invoked
return $this->execute($request, function (HTTPRequest $request) { return $this->execute($request, static function (HTTPRequest $request) {
return Director::singleton()->handleRequest($request); return Director::singleton()->handleRequest($request);
}, $flush); }, $flush);
} }
@ -55,6 +124,7 @@ class HTTPApplication implements Application
* @param HTTPRequest $request * @param HTTPRequest $request
* @param callable $callback * @param callable $callback
* @param bool $flush * @param bool $flush
*
* @return HTTPResponse * @return HTTPResponse
*/ */
public function execute(HTTPRequest $request, callable $callback, $flush = false) public function execute(HTTPRequest $request, callable $callback, $flush = false)

View File

@ -13,8 +13,10 @@ use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
/** /**
* Allows events to be registered and passed through middleware. * Implements the following URL normalisation rules
* Useful for event registered prior to the beginning of a middleware chain. * - redirect basic auth requests to HTTPS
* - force WWW, redirect to the subdomain "www."
* - force SSL, redirect to https
*/ */
class CanonicalURLMiddleware implements HTTPMiddleware class CanonicalURLMiddleware implements HTTPMiddleware
{ {

View File

@ -8,7 +8,7 @@ use SilverStripe\Core\Injector\Injectable;
/** /**
* Handles internal change detection via etag / ifmodifiedsince headers, * Handles internal change detection via etag / ifmodifiedsince headers,
* conditonally sending a 304 not modified if possible. * conditionally sending a 304 not modified if possible.
*/ */
class ChangeDetectionMiddleware implements HTTPMiddleware class ChangeDetectionMiddleware implements HTTPMiddleware
{ {

View File

@ -0,0 +1,295 @@
<?php
namespace SilverStripe\Control\Middleware;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Security\Confirmation;
use SilverStripe\Security\Security;
/**
* Checks whether user manual confirmation is required for HTTPRequest
* depending on the rules given.
*
* How it works:
* - Gives the request to every single rule
* - If no confirmation items are found by the rules, then move on to the next middleware
* - initialize the Confirmation\Storage with all the confirmation items found
* - Check whether the storage has them confirmed already and if yes, move on to the next middleware
* - Otherwise redirect to the confirmation URL
*/
class ConfirmationMiddleware implements HTTPMiddleware
{
/**
* The confirmation storage identifier
*
* @var string
*/
protected $confirmationId = 'middleware';
/**
* Confirmation form URL
*
* @var string
*/
protected $confirmationFormUrl = '/dev/confirm';
/**
* The list of rules to check requests against
*
* @var ConfirmationMiddleware\Rule[]
*/
protected $rules;
/**
* The list of bypasses
*
* @var ConfirmationMiddleware\Bypass[]
*/
protected $bypasses = [];
/**
* Where user should be redirected when refusing
* the action on the confirmation form
*
* @var string
*/
private $declineUrl;
/**
* Init the middleware with the rules
*
* @param ConfirmationMiddleware\Rule[] $rules Rules to check requests against
*/
public function __construct(...$rules)
{
$this->rules = $rules;
$this->declineUrl = Director::baseURL();
}
/**
* The URL of the confirmation form ("Security/confirm/middleware" by default)
*
* @param HTTPRequest $request Active request
* @param string $confirmationStorageId ID of the confirmation storage to be used
*
* @return string URL of the confirmation form
*/
protected function getConfirmationUrl(HTTPRequest $request, $confirmationStorageId)
{
return Controller::join_links(
$this->confirmationFormUrl,
urlencode($confirmationStorageId)
);
}
/**
* Returns the URL where the user to be redirected
* when declining the action (on the confirmation form)
*
* @param HTTPRequest $request Active request
*
* @return string URL
*/
protected function generateDeclineUrlForRequest(HTTPRequest $request)
{
return $this->declineUrl;
}
/**
* Override the default decline url
*
* @param string $url
*
* @return $this
*/
public function setDeclineUrl($url)
{
$this->declineUrl = $url;
return $this;
}
/**
* Check whether the rules can be bypassed
* without user confirmation
*
* @param HTTPRequest $request
*
* @return bool
*/
public function canBypass(HTTPRequest $request)
{
foreach ($this->bypasses as $bypass) {
if ($bypass->checkRequestForBypass($request)) {
return true;
}
}
return false;
}
/**
* Extract the confirmation items from the request and return
*
* @param HTTPRequest $request
*
* @return Confirmation\Item[] list of confirmation items
*/
public function getConfirmationItems(HTTPRequest $request)
{
$confirmationItems = [];
foreach ($this->rules as $rule) {
if ($item = $rule->getRequestConfirmationItem($request)) {
$confirmationItems[] = $item;
}
}
return $confirmationItems;
}
/**
* Initialize the confirmation session storage
* with the confirmation items and return an HTTPResponse
* redirecting to the according confirmation form.
*
* @param HTTPRequest $request
* @param Confirmation\Storage $storage
* @param Confirmation\Item[] $confirmationItems
*
* @return HTTPResponse
*/
protected function buildConfirmationRedirect(HTTPRequest $request, Confirmation\Storage $storage, array $confirmationItems)
{
$storage->cleanup();
foreach ($confirmationItems as $item) {
$storage->putItem($item);
}
$storage->setSuccessRequest($request);
$storage->setFailureUrl($this->generateDeclineUrlForRequest($request));
$result = new HTTPResponse();
$result->redirect($this->getConfirmationUrl($request, $this->confirmationId));
return $result;
}
/**
* Process the confirmation items and either perform the confirmedEffect
* and pass the request to the next middleware, or return a redirect to
* the confirmation form
*
* @param HTTPRequest $request
* @param callable $delegate
* @param Confirmation\Item[] $items
*
* @return HTTPResponse
*/
protected function processItems(HTTPRequest $request, callable $delegate, $items)
{
$storage = Injector::inst()->createWithArgs(Confirmation\Storage::class, [$request->getSession(), $this->confirmationId, false]);
if (!count($storage->getItems())) {
return $this->buildConfirmationRedirect($request, $storage, $items);
}
$confirmed = false;
if ($storage->getHttpMethod() === 'POST') {
$postVars = $request->postVars();
$csrfToken = $storage->getCsrfToken();
$confirmed = $storage->confirm($postVars) && isset($postVars[$csrfToken]);
} else {
$confirmed = $storage->check($items);
}
if (!$confirmed) {
return $this->buildConfirmationRedirect($request, $storage, $items);
}
if ($response = $this->confirmedEffect($request)) {
return $response;
}
$storage->cleanup();
return $delegate($request);
}
/**
* The middleware own effects that should be performed on confirmation
*
* This method is getting called before the confirmation storage cleanup
* so that any responses returned here don't trigger a new confirmtation
* for the same request traits
*
* @param HTTPRequest $request
*
* @return null|HTTPResponse
*/
protected function confirmedEffect(HTTPRequest $request)
{
return null;
}
public function process(HTTPRequest $request, callable $delegate)
{
if ($this->canBypass($request)) {
if ($response = $this->confirmedEffect($request)) {
return $response;
} else {
return $delegate($request);
}
}
if (!$items = $this->getConfirmationItems($request)) {
return $delegate($request);
}
return $this->processItems($request, $delegate, $items);
}
/**
* Override the confirmation storage ID
*
* @param string $id
*
* @return $this
*/
public function setConfirmationStorageId($id)
{
$this->confirmationId = $id;
return $this;
}
/**
* Override the confirmation form url
*
* @param string $url
*
* @return $this
*/
public function setConfirmationFormUrl($url)
{
$this->confirmationFormUrl = $url;
return $this;
}
/**
* Set the list of bypasses for the confirmation
*
* @param ConfirmationMiddleware\Bypass[] $bypasses
*
* @return $this
*/
public function setBypasses($bypasses)
{
$this->bypasses = $bypasses;
return $this;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
/**
* Bypass for AJAX requests
*
* Relies on HTTPRequest::isAjax implementation
*/
class AjaxBypass implements Bypass
{
/**
* Returns true for AJAX requests
*
* @param HTTPRequest $request
*
* @return bool
*/
public function checkRequestForBypass(HTTPRequest $request)
{
return $request->isAjax();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\Confirmation;
/**
* A bypass for manual confirmation by user (depending on some runtime conditions)
*/
interface Bypass
{
/**
* Check the request for whether we can bypass
* the confirmation
*
* @param HTTPRequest $request
*
* @return bool True if we can bypass, False if the confirmation is required
*/
public function checkRequestForBypass(HTTPRequest $request);
}

View File

@ -0,0 +1,25 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Kernel;
/**
* Allows a bypass when the request has been run in CLI mode
*/
class CliBypass implements Bypass
{
/**
* Returns true if the current process is running in CLI mode
*
* @param HTTPRequest $request
*
* @return bool
*/
public function checkRequestForBypass(HTTPRequest $request)
{
return Director::is_cli();
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Kernel;
/**
* Allows a bypass for a list of environment types (e.g. DEV, TEST, LIVE)
*/
class EnvironmentBypass implements Bypass
{
/**
* The list of environments allowing a bypass for a confirmation
*
* @var string[]
*/
private $environments;
/**
* Initialize the bypass with the list of environment types
*
* @param string[] ...$environments
*/
public function __construct(...$environments)
{
$this->environments = $environments;
}
/**
* Returns the list of environments
*
* @return string[]
*
*/
public function getEnvironments()
{
return $this->environments;
}
/**
* Set the list of environments allowing a bypass
*
* @param string[] $environments List of environment types
*
* @return $this
*/
public function setEnvironments($environments)
{
$this->environments = $environments;
return $this;
}
/**
* Checks whether the current environment type in the list
* of allowed ones
*
* @param HTTPRequest $request
*
* @return bool
*/
public function checkRequestForBypass(HTTPRequest $request)
{
return in_array(Director::get_environment_type(), $this->environments, true);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\Confirmation;
/**
* A rule to match a GET parameter within HTTPRequest
*/
class GetParameter implements Rule, Bypass
{
/**
* Parameter name
*
* @var string
*/
private $name;
/**
* Initialize the rule with a parameter name
*
* @param string $name
*/
public function __construct($name)
{
$this->setName($name);
}
/**
* Return the parameter name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the parameter name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Generates the confirmation item
*
* @param string $token
*
* @return Confirmation\Item
*/
protected function buildConfirmationItem($token, $value)
{
return new Confirmation\Item(
$token,
_t(__CLASS__.'.CONFIRMATION_NAME', '"{key}" GET parameter', ['key' => $this->name]),
sprintf('%s = "%s"', $this->name, $value)
);
}
/**
* Generates the unique token depending on the path and the parameter
*
* @param string $path URL path
* @param string $param The parameter value
*
* @return string
*/
protected function generateToken($path, $value)
{
return sprintf('%s::%s?%s=%s', static::class, $path, $this->name, $value);
}
/**
* Check request contains the GET parameter
*
* @param HTTPRequest $request
*
* @return bool
*/
protected function checkRequestHasParameter(HTTPRequest $request)
{
return array_key_exists($this->name, $request->getVars());
}
public function checkRequestForBypass(HTTPRequest $request)
{
return $this->checkRequestHasParameter($request);
}
public function getRequestConfirmationItem(HTTPRequest $request)
{
if (!$this->checkRequestHasParameter($request)) {
return null;
}
$path = $request->getURL();
$value = $request->getVar($this->name);
$token = $this->generateToken($path, $value);
return $this->buildConfirmationItem($token, $value);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
/**
* Allows to bypass requests of a particular HTTP method
*/
class HttpMethodBypass implements Bypass
{
/**
* HTTP Methods to bypass
*
* @var string[]
*/
private $methods = [];
/**
* Initialize the bypass with HTTP methods
*
* @param string[] ...$method
*/
public function __construct(...$methods)
{
$this->addMethods(...$methods);
}
/**
* Returns the list of methods
*
* @return string[]
*/
public function getMethods()
{
return $this->methods;
}
/**
* Add new HTTP methods to the list
*
* @param string[] ...$methods
*
* return $this
*/
public function addMethods(...$methods)
{
// uppercase and exclude empties
$methods = array_reduce(
$methods,
static function &(&$result, $method) {
$method = strtoupper(trim($method));
if (strlen($method)) {
$result[] = $method;
}
return $result;
},
[]
);
foreach ($methods as $method) {
if (!in_array($method, $this->methods, true)) {
$this->methods[] = $method;
}
}
return $this;
}
/**
* Returns true if the current process is running in CLI mode
*
* @param HTTPRequest $request
*
* @return bool
*/
public function checkRequestForBypass(HTTPRequest $request)
{
return in_array($request->httpMethod(), $this->methods, true);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
/**
* Path aware trait for rules and bypasses
*/
trait PathAware
{
/**
* @var string
*/
private $path;
/**
* Returns the path
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Update the path
*
* @param string $path
*
* @return $this
*/
public function setPath($path)
{
$this->path = $this->normalisePath($path);
return $this;
}
/**
* Returns the normalised version of the given path
*
* @param string $path Path to normalise
*
* @return string normalised version of the path
*/
protected function normalisePath($path)
{
if (substr($path, -1) !== '/') {
return $path . '/';
} else {
return $path;
}
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\Confirmation;
/**
* A rule for checking whether we need to protect a Request
*/
interface Rule
{
/**
* Check the request by the rule and return
* a confirmation item
*
* @param HTTPRequest $request
*
* @return null|Confirmation\Item Confirmation item if necessary to protect the request or null otherwise
*/
public function getRequestConfirmationItem(HTTPRequest $request);
}

View File

@ -0,0 +1,197 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\Confirmation;
/**
* A rule to match a particular URL
*/
class Url implements Rule, Bypass
{
use PathAware;
/**
* The HTTP methods
*
* @var HttpMethodBypass
*/
private $httpMethods;
/**
* The list of GET parameters URL should have to match
*
* @var array keys are parameter names, values are strings to match or null if any
*/
private $params = null;
/**
* Initialize the rule with the parameters
*
* @param string $path url path to check for
* @param string[]|string|null $httpMethods to match against
* @param string[]|null $params a list of GET parameters
*/
public function __construct($path, $httpMethods = null, $params = null)
{
$this->setPath($path);
$this->setParams($params);
$this->httpMethods = new HttpMethodBypass();
if (is_array($httpMethods)) {
$this->addHttpMethods(...$httpMethods);
} elseif (!is_null($httpMethods)) {
$this->addHttpMethods($httpMethods);
}
}
/**
* Add HTTP methods to check against
*
* @param string[] ...$methods
*
* @return $this
*/
public function addHttpMethods(...$methods)
{
$this->httpMethods->addMethods(...$methods);
return $this;
}
/**
* Returns HTTP methods to be checked
*
* @return string[]
*/
public function getHttpMethods()
{
return $this->httpMethods->getMethods();
}
/**
* Set the GET parameters
* null to skip parameter check
*
* If an array of parameters provided,
* then URL should contain ALL of them and
* ONLY them to match. If the values in the list
* contain strings, those will be checked
* against parameter values accordingly. Null
* as a value in the array matches any parameter values.
*
* @param string|null $httpMethods
*
* @return $this
*/
public function setParams($params = null)
{
$this->params = $params;
return $this;
}
public function checkRequestForBypass(HTTPRequest $request)
{
return $this->checkRequest($request);
}
public function getRequestConfirmationItem(HTTPRequest $request)
{
if (!$this->checkRequest($request)) {
return null;
}
$fullPath = $request->getURL(true);
$token = $this->generateToken($request->httpMethod(), $fullPath);
return $this->buildConfirmationItem($token, $fullPath);
}
/**
* Match the request against the rules
*
* @param HTTPRequest $request
*
* @return bool
*/
public function checkRequest(HTTPRequest $request)
{
$httpMethods = $this->getHttpMethods();
if (count($httpMethods) && !in_array($request->httpMethod(), $httpMethods, true)) {
return false;
}
if (!$this->checkPath($request->getURL())) {
return false;
}
if (!is_null($this->params)) {
$getVars = $request->getVars();
// compare the request parameters with the declared ones
foreach ($this->params as $key => $val) {
if (is_null($val)) {
$cmp = array_key_exists($key, $getVars);
} else {
$cmp = isset($getVars[$key]) && $getVars[$key] === strval($val);
}
if (!$cmp) {
return false;
}
}
// check only declared parameters exist in the request
foreach ($getVars as $key => $val) {
if (!array_key_exists($key, $this->params)) {
return false;
}
}
}
return true;
}
/**
* Checks the given path by the rules and
* returns true if it is matching
*
* @param string $path Path to be checked
*
* @return bool
*/
protected function checkPath($path)
{
return $this->getPath() === $this->normalisePath($path);
}
/**
* Generates the confirmation item
*
* @param string $token
* @param string $url
*
* @return Confirmation\Item
*/
protected function buildConfirmationItem($token, $url)
{
return new Confirmation\Item(
$token,
_t(__CLASS__.'.CONFIRMATION_NAME', 'URL is protected'),
_t(__CLASS__.'.CONFIRMATION_DESCRIPTION', 'The URL is: "{url}"', ['url' => $url])
);
}
/**
* Generates the unique token depending on the path
*
* @param string $path URL path
*
* @return string
*/
protected function generateToken($httpMethod, $path)
{
return sprintf('%s::%s|%s', static::class, $httpMethod, $path);
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\Confirmation;
/**
* A rule to match beginning of URL
*/
class UrlPathStartswith implements Rule, Bypass
{
use PathAware;
/**
* Initialize the rule with the path
*
* @param string $path
*/
public function __construct($path)
{
$this->setPath($path);
}
/**
* Generates the confirmation item
*
* @param string $token
* @param string $url
*
* @return Confirmation\Item
*/
protected function buildConfirmationItem($token, $url)
{
return new Confirmation\Item(
$token,
_t(__CLASS__.'.CONFIRMATION_NAME', 'URL begins with "{path}"', ['path' => $this->getPath()]),
_t(__CLASS__.'.CONFIRMATION_DESCRIPTION', 'The complete URL is: "{url}"', ['url' => $url])
);
}
/**
* Generates the unique token depending on the path
*
* @param string $path URL path
*
* @return string
*/
protected function generateToken($path)
{
return sprintf('%s::%s', static::class, $path);
}
/**
* Checks the given path by the rules and
* returns whether it should be protected
*
* @param string $path Path to be checked
*
* @return bool
*/
protected function checkPath($path)
{
$targetPath = $this->getPath();
return strncmp($this->normalisePath($path), $targetPath, strlen($targetPath)) === 0;
}
public function checkRequestForBypass(HTTPRequest $request)
{
return $this->checkPath($request->getURL());
}
public function getRequestConfirmationItem(HTTPRequest $request)
{
if (!$this->checkPath($request->getURL())) {
return null;
}
$token = $this->generateToken($this->getPath());
return $this->buildConfirmationItem($token, $request->getURL(true));
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\Confirmation;
/**
* A case insensitive rule to match beginning of URL
*/
class UrlPathStartswithCaseInsensitive extends UrlPathStartswith
{
protected function checkPath($path)
{
$pattern = $this->getPath();
$mb_path = mb_strcut($this->normalisePath($path), 0, strlen($pattern));
return mb_stripos($mb_path, $pattern) === 0;
}
}

View File

@ -9,7 +9,7 @@ use SilverStripe\Control\HTTPResponse;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
/** /**
* Display execution metricts for the current request if in dev mode and `execmetric` is provided as a request variable. * Display execution metrics for the current request if in dev mode and `execmetric` is provided as a request variable.
*/ */
class ExecMetricMiddleware implements HTTPMiddleware class ExecMetricMiddleware implements HTTPMiddleware
{ {

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Control\Middleware; namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Flushable; use SilverStripe\Core\Flushable;
@ -11,12 +12,9 @@ use SilverStripe\Core\Flushable;
*/ */
class FlushMiddleware implements HTTPMiddleware class FlushMiddleware implements HTTPMiddleware
{ {
/**
* @inheritdoc
*/
public function process(HTTPRequest $request, callable $delegate) public function process(HTTPRequest $request, callable $delegate)
{ {
if (array_key_exists('flush', $request->getVars())) { if (Director::isManifestFlushed()) {
// Disable cache when flushing // Disable cache when flushing
HTTPCacheControlMiddleware::singleton()->disableCache(true); HTTPCacheControlMiddleware::singleton()->disableCache(true);

View File

@ -0,0 +1,159 @@
<?php
namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
/**
* Extends the ConfirmationMiddleware with checks for user permissions
*
* Respects users who don't have enough access and does not
* ask them for confirmation
*
* By default it enforces authentication by redirecting users to a login page.
*
* How it works:
* - if user can bypass the middleware, then pass request further
* - if there are no confirmation items, then pass request further
* - if user is not authenticated and enforceAuthentication is false, then pass request further
* - if user does not have at least one of the affected permissions, then pass request further
* - otherwise, pass handling to the parent (ConfirmationMiddleware)
*/
class PermissionAwareConfirmationMiddleware extends ConfirmationMiddleware
{
/**
* List of permissions affected by the middleware
*
* @see setAffectedPermissions method for more details
*
* @var string[]
*/
private $affectedPermissions = [];
/**
* Wthether the middleware should redirect to a login form
* if the user is not authenticated
*
* @var bool
*/
private $enforceAuthentication = true;
/**
* Returns the list of permissions that are affected
*
* @return string[]
*/
public function getAffectedPermissions()
{
return $this->affectedPermissions;
}
/**
* Set the list of affected permissions
*
* If the user doesn't have at least one of these, we assume they
* don't have access to the protected action, so we don't ask
* for a confirmation
*
* @param string[] $permissions list of affected permissions
*
* @return $this
*/
public function setAffectedPermissions($permissions)
{
$this->affectedPermissions = $permissions;
return $this;
}
/**
* Returns flag whether we want to enforce authentication or not
*
* @return bool
*/
public function getEnforceAuthentication()
{
return $this->enforceAuthentication;
}
/**
* Set whether we want to enforce authentication
*
* We either enforce authentication (redirect to a login form)
* or silently assume the user does not have permissions and
* so we don't have to ask for a confirmation
*
* @param bool $enforce
*
* @return $this
*/
public function setEnforceAuthentication($enforce)
{
$this->enforceAuthentication = $enforce;
return $this;
}
/**
* Check whether the user has permissions to perform the target operation
* Otherwise we may want to skip the confirmation dialog.
*
* WARNING! The user has to be authenticated beforehand
*
* @param HTTPRequest $request
*
* @return bool
*/
public function hasAccess(HTTPRequest $request)
{
foreach ($this->getAffectedPermissions() as $permission) {
if (Permission::check($permission)) {
return true;
}
}
return false;
}
/**
* Returns HTTPResponse with a redirect to a login page
*
* @param HTTPRequest $request
*
* @return HTTPResponse redirect to a login page
*/
protected function getAuthenticationRedirect(HTTPRequest $request)
{
$backURL = $request->getURL(true);
$loginPage = sprintf(
'%s?BackURL=%s',
Director::absoluteURL(Security::config()->get('login_url')),
urlencode($backURL)
);
$result = new HTTPResponse();
$result->redirect($loginPage);
return $result;
}
protected function processItems(HTTPRequest $request, callable $delegate, $items)
{
if (!Security::getCurrentUser()) {
if ($this->getEnforceAuthentication()) {
return $this->getAuthenticationRedirect($request);
} else {
// assume the user does not have permissions anyway
return $delegate($request);
}
}
if (!$this->hasAccess($request)) {
return $delegate($request);
}
return parent::processItems($request, $delegate, $items);
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\Middleware\URLSpecialsMiddleware\FlushScheduler;
use SilverStripe\Control\Middleware\URLSpecialsMiddleware\SessionEnvTypeSwitcher;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Security\RandomGenerator;
/**
* Check the request for the URL special variables.
* Performs authorisation, confirmation and actions for some of those.
*
* WARNING: Bypasses only disable authorisation and confirmation, but not actions nor redirects
*
* The rules are:
* - flush GET parameter
* - isDev GET parameter
* - isTest GET parameter
* - dev/build URL
*
* @see https://docs.silverstripe.org/en/4/developer_guides/debugging/url_variable_tools/ special variables docs
*
* {@inheritdoc}
*/
class URLSpecialsMiddleware extends PermissionAwareConfirmationMiddleware
{
use FlushScheduler;
use SessionEnvTypeSwitcher;
/**
* Initializes the middleware with the required rules
*/
public function __construct()
{
parent::__construct(
new ConfirmationMiddleware\GetParameter("flush"),
new ConfirmationMiddleware\GetParameter("isDev"),
new ConfirmationMiddleware\GetParameter("isTest"),
new ConfirmationMiddleware\UrlPathStartswith("dev/build")
);
}
/**
* Looks up for the special flags passed in the request
* and schedules the changes accordingly for the next request.
* Returns a redirect to the same page (with a random token) if
* there are changes introduced by the flags.
* Returns null if there is no impact introduced by the flags.
*
* @param HTTPRequest $request
*
* @return null|HTTPResponse redirect to the same url
*/
public function buildImpactRedirect(HTTPRequest $request)
{
$flush = $this->scheduleFlush($request);
$env_type = $this->setSessionEnvType($request);
if ($flush || $env_type) {
// the token only purpose is to invalidate browser/proxy cache
$request['urlspecialstoken'] = bin2hex(random_bytes(4));
$result = new HTTPResponse();
$result->redirect('/' . $request->getURL(true));
return $result;
}
}
protected function confirmedEffect(HTTPRequest $request)
{
if ($response = $this->buildImpactRedirect($request)) {
HTTPCacheControlMiddleware::singleton()->disableCache(true);
return $response;
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace SilverStripe\Control\Middleware\URLSpecialsMiddleware;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Startup\ScheduledFlushDiscoverer;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
/**
* Schedule flush operation for a following request
*
* The scheduler does not trigger a flush but rather puts a marker
* into the manifest cache so that one of the next Requests can
* find it and perform the actual manifest flush.
*/
trait FlushScheduler
{
/**
* Schedules the manifest flush operation for a following request
*
* WARNING! Does not perform flush, but schedules it for another request
*
* @param HTTPRequest $request
*
* @return bool true if flush has been scheduled, false otherwise
*/
public function scheduleFlush(HTTPRequest $request)
{
$flush = array_key_exists('flush', $request->getVars()) || ($request->getURL() === 'dev/build');
if (!$flush || Director::isManifestFlushed()) {
return false;
}
$kernel = Injector::inst()->get(Kernel::class);
ScheduledFlushDiscoverer::scheduleFlush($kernel);
return true;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace SilverStripe\Control\Middleware\URLSpecialsMiddleware;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Startup\ScheduledFlushDiscoverer;
use SilverStripe\Control\HTTPRequest;
/**
* Implements switching user session into Test and Dev environment types
*/
trait SessionEnvTypeSwitcher
{
/**
* Checks whether the request has GET flags to control
* environment type and amends the user session accordingly
*
* @param HTTPRequest $request
*
* @return bool true if changed the user session state, false otherwise
*/
public function setSessionEnvType(HTTPRequest $request)
{
$session = $request->getSession();
if (array_key_exists('isTest', $request->getVars())) {
if (!is_null($isTest = $request->getVar('isTest'))) {
if ($isTest === $session->get('isTest')) {
return false;
}
}
$session->clear('isDev');
$session->set('isTest', $isTest);
return true;
} elseif (array_key_exists('isDev', $request->getVars())) {
if (!is_null($isDev = $request->getVar('isDev'))) {
if ($isDev === $session->get('isDev')) {
return false;
}
}
$session->clear('isTest');
$session->set('isDev', $isDev);
return true;
}
return false;
}
}

View File

@ -77,6 +77,21 @@ class CoreKernel implements Kernel
protected $basePath = null; protected $basePath = null;
/**
* Indicates whether the Kernel has been booted already
*
* @var bool
*/
private $booted = false;
/**
* Indicates whether the Kernel has been flushed on boot
* Unitialized before boot
*
* @var bool
*/
private $flush;
/** /**
* Create a new kernel for this application * Create a new kernel for this application
* *
@ -126,6 +141,13 @@ class CoreKernel implements Kernel
$this->setThemeResourceLoader($themeResourceLoader); $this->setThemeResourceLoader($themeResourceLoader);
} }
/**
* Get the environment type
*
* @return string
*
* @deprecated 5.0 use Director::get_environment_type() instead. Since 5.0 it should return only if kernel overrides. No checking SESSION or Environment.
*/
public function getEnvironment() public function getEnvironment()
{ {
// Check set // Check set
@ -151,41 +173,23 @@ class CoreKernel implements Kernel
* Check or update any temporary environment specified in the session. * Check or update any temporary environment specified in the session.
* *
* @return null|string * @return null|string
*
* @deprecated 5.0 Use Director::get_session_environment_type() instead
*/ */
protected function sessionEnvironment() protected function sessionEnvironment()
{ {
// Check isDev in querystring if (!$this->booted) {
if (isset($_GET['isDev'])) { // session is not initialyzed yet, neither is manifest
if (isset($_SESSION)) {
unset($_SESSION['isTest']); // In case we are changing from test mode
$_SESSION['isDev'] = $_GET['isDev'];
}
return self::DEV;
}
// Check isTest in querystring
if (isset($_GET['isTest'])) {
if (isset($_SESSION)) {
unset($_SESSION['isDev']); // In case we are changing from dev mode
$_SESSION['isTest'] = $_GET['isTest'];
}
return self::TEST;
}
// Check session
if (!empty($_SESSION['isDev'])) {
return self::DEV;
}
if (!empty($_SESSION['isTest'])) {
return self::TEST;
}
// no session environment
return null; return null;
} }
return Director::get_session_environment_type();
}
public function boot($flush = false) public function boot($flush = false)
{ {
$this->flush = $flush;
$this->bootPHP(); $this->bootPHP();
$this->bootManifests($flush); $this->bootManifests($flush);
$this->bootErrorHandling(); $this->bootErrorHandling();
@ -193,6 +197,8 @@ class CoreKernel implements Kernel
$this->bootConfigs(); $this->bootConfigs();
$this->bootDatabaseGlobals(); $this->bootDatabaseGlobals();
$this->validateDatabase(); $this->validateDatabase();
$this->booted = true;
} }
/** /**
@ -655,4 +661,14 @@ class CoreKernel implements Kernel
$this->themeResourceLoader = $themeResourceLoader; $this->themeResourceLoader = $themeResourceLoader;
return $this; return $this;
} }
/**
* Returns whether the Kernel has been flushed on boot
*
* @return bool|null null if the kernel hasn't been booted yet
*/
public function isFlushed()
{
return $this->flush;
}
} }

View File

@ -222,4 +222,14 @@ class Environment
{ {
static::$env[$name] = $value; static::$env[$name] = $value;
} }
/**
* Returns true if this script is being run from the command line rather than the web server
*
* @return bool
*/
public static function isCli()
{
return in_array(strtolower(php_sapi_name()), ['cli', 'phpdbg']);
}
} }

View File

@ -167,6 +167,14 @@ class ClassManifest
*/ */
private $visitor; private $visitor;
/**
* Indicates whether the cache has been
* regenerated in the current process
*
* @var bool
*/
private $cacheRegenerated = false;
/** /**
* Constructs and initialises a new class manifest, either loading the data * Constructs and initialises a new class manifest, either loading the data
* from the cache or re-scanning for classes. * from the cache or re-scanning for classes.
@ -181,6 +189,72 @@ class ClassManifest
$this->cacheKey = 'manifest'; $this->cacheKey = 'manifest';
} }
private function buildCache($includeTests = false)
{
if ($this->cache) {
return $this->cache;
} elseif (!$this->cacheFactory) {
return null;
} else {
return $this->cacheFactory->create(
CacheInterface::class . '.classmanifest',
['namespace' => 'classmanifest' . ($includeTests ? '_tests' : '')]
);
}
}
/**
* @internal This method is not a part of public API and will be deleted without a deprecation warning
*
* @return int
*/
public function getManifestTimestamp($includeTests = false)
{
$cache = $this->buildCache($includeTests);
if (!$cache) {
return null;
}
return $cache->get('generated_at');
}
/**
* @internal This method is not a part of public API and will be deleted without a deprecation warning
*/
public function scheduleFlush($includeTests = false)
{
$cache = $this->buildCache($includeTests);
if (!$cache) {
return null;
}
$cache->set('regenerate', true);
}
/**
* @internal This method is not a part of public API and will be deleted without a deprecation warning
*/
public function isFlushScheduled($includeTests = false)
{
$cache = $this->buildCache($includeTests);
if (!$cache) {
return null;
}
return $cache->get('regenerate');
}
/**
* @internal This method is not a part of public API and will be deleted without a deprecation warning
*/
public function isFlushed()
{
return $this->cacheRegenerated;
}
/** /**
* Initialise the class manifest * Initialise the class manifest
* *
@ -189,13 +263,7 @@ class ClassManifest
*/ */
public function init($includeTests = false, $forceRegen = false) public function init($includeTests = false, $forceRegen = false)
{ {
// build cache from factory $this->cache = $this->buildCache($includeTests);
if ($this->cacheFactory) {
$this->cache = $this->cacheFactory->create(
CacheInterface::class . '.classmanifest',
['namespace' => 'classmanifest' . ($includeTests ? '_tests' : '')]
);
}
// Check if cache is safe to use // Check if cache is safe to use
if (!$forceRegen if (!$forceRegen
@ -458,7 +526,11 @@ class ClassManifest
if ($this->cache) { if ($this->cache) {
$data = $this->getState(); $data = $this->getState();
$this->cache->set($this->cacheKey, $data); $this->cache->set($this->cacheKey, $data);
$this->cache->set('generated_at', time());
$this->cache->delete('regenerate');
} }
$this->cacheRegenerated = true;
} }
/** /**

View File

@ -14,6 +14,8 @@ use SilverStripe\Security\RandomGenerator;
* string parameters * string parameters
* *
* @internal This class is designed specifically for use pre-startup and may change without warning * @internal This class is designed specifically for use pre-startup and may change without warning
*
* @deprecated 5.0 To be removed in SilverStripe 5.0
*/ */
abstract class AbstractConfirmationToken abstract class AbstractConfirmationToken
{ {

View File

@ -0,0 +1,31 @@
<?php
namespace SilverStripe\Core\Startup;
/**
* Handle a callable object as a discoverer
*/
class CallbackFlushDiscoverer implements FlushDiscoverer
{
/**
* Callback incapsulating the discovery logic
*
* @var Callable
*/
protected $callback;
/**
* Construct the discoverer from a callback
*
* @param Callable $callback returning FlushDiscoverer response or a timestamp
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
public function shouldFlush()
{
return call_user_func($this->callback);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\Core\Startup;
/**
* Implements the composite over flush discoverers
*
* @see https://en.wikipedia.org/wiki/Composite_pattern composite design pattern for more information
*/
class CompositeFlushDiscoverer extends \ArrayIterator implements FlushDiscoverer
{
public function shouldFlush()
{
foreach ($this as $discoverer) {
$flush = $discoverer->shouldFlush();
if (!is_null($flush)) {
return $flush;
}
}
}
}

View File

@ -12,6 +12,8 @@ use SilverStripe\Core\Convert;
* check multiple tokens at once without having to potentially redirect the user for each of them * 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 * @internal This class is designed specifically for use pre-startup and may change without warning
*
* @deprecated 5.0 To be removed in SilverStripe 5.0
*/ */
class ConfirmationTokenChain class ConfirmationTokenChain
{ {

View File

@ -0,0 +1,120 @@
<?php
namespace SilverStripe\Core\Startup;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Environment;
/**
* Checks whether a filesystem resource has been changed since
* the manifest generation
*
* For this discoverer to get activated you should define SS_FLUSH_ON_DEPLOY
* variable
* - if the environment variable SS_FLUSH_ON_DEPLOY undefined or `false`, then does nothing
* - if SS_FLUSH_ON_DEPLOY is true, then checks __FILE__ modification time
* - otherwise takes {BASE_PATH/SS_FLUSH_ON_DEPLOY} as the resource to check
*
* Examples:
*
* - `SS_FLUSH_ON_DEPLOY=""` would check the BASE_PATH folder for modifications (not the files within)
* - `SS_FLUSH_ON_DEPLOY=true` would check BASE_PATH/vendor/silverstripe/framework/src/Core/Startup/DeployFlushDiscoverer.php
* file modification
* - `SS_FLUSH_ON_DEPLOY=false` disable filesystem checks
* - `SS_FLUSH_ON_DEPLOY="public/index.php"` checks BASE_PATH/public/index.php file modification time
*/
class DeployFlushDiscoverer implements FlushDiscoverer
{
/**
* Active kernel
*
* @var Kernel
*/
protected $kernel;
public function __construct(Kernel $kernel)
{
$this->kernel = $kernel;
}
/**
* Returns the timestamp of the manifest generation or null
* if no cache has been found (or couldn't read the cache)
*
* @return int|null unix timestamp
*/
protected function getCacheTimestamp()
{
$classLoader = $this->kernel->getClassLoader();
$classManifest = $classLoader->getManifest();
$cacheTimestamp = $classManifest->getManifestTimestamp();
return $cacheTimestamp;
}
/**
* Returns the resource to be checked for deployment
*
* - if the environment variable SS_FLUSH_ON_DEPLOY undefined or false, then returns null
* - if SS_FLUSH_ON_DEPLOY is true, then takes __FILE__ as the resource to check
* - otherwise takes {BASE_PATH/SS_FLUSH_ON_DEPLOY} as the resource to check
*
* @return string|null returns the resource path or null if not set
*/
protected function getDeployResource()
{
$resource = Environment::getEnv('SS_FLUSH_ON_DEPLOY');
if ($resource === false) {
return null;
}
if ($resource === true) {
$path = __FILE__;
} else {
$path = sprintf("%s/%s", BASE_PATH, $resource);
}
return $path;
}
/**
* Returns the resource modification timestamp
*
* @param string $resource Path to the filesystem
*
* @return int
*/
protected function getDeployTimestamp($resource)
{
if (!file_exists($resource)) {
return 0;
}
return max(filemtime($resource), filectime($resource));
}
/**
* Returns true if the deploy timestamp greater than the cache generation timestamp
*
* {@inheritdoc}
*/
public function shouldFlush()
{
$resource = $this->getDeployResource();
if (is_null($resource)) {
return null;
}
$deploy = $this->getDeployTimestamp($resource);
$cache = $this->getCacheTimestamp();
if ($deploy && $cache && $deploy > $cache) {
return true;
}
return null;
}
}

View File

@ -16,6 +16,8 @@ use Exception;
* $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute(); * $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute();
* *
* @internal This class is designed specifically for use pre-startup and may change without warning * @internal This class is designed specifically for use pre-startup and may change without warning
*
* @deprecated 5.0 To be removed in SilverStripe 5.0
*/ */
class ErrorControlChain class ErrorControlChain
{ {

View File

@ -8,12 +8,15 @@ use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Middleware\HTTPMiddleware; use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Core\Application; use SilverStripe\Core\Application;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
/** /**
* Decorates application bootstrapping with errorcontrolchain * Decorates application bootstrapping with errorcontrolchain
* *
* @internal This class is designed specifically for use pre-startup and may change without warning * @internal This class is designed specifically for use pre-startup and may change without warning
*
* @deprecated 5.0 To be removed in SilverStripe 5.0
*/ */
class ErrorControlChainMiddleware implements HTTPMiddleware class ErrorControlChainMiddleware implements HTTPMiddleware
{ {
@ -22,14 +25,24 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
*/ */
protected $application = null; protected $application = null;
/**
* Whether to keep working (legacy mode)
*
* @var bool
*/
private $legacy;
/** /**
* Build error control chain for an application * Build error control chain for an application
* *
* @param Application $application * @param Application $application
* @param bool $legacy Keep working (legacy mode)
*/ */
public function __construct(Application $application) public function __construct(Application $application, $legacy = false)
{ {
$this->application = $application; $this->application = $application;
$this->legacy = $legacy;
Deprecation::notice('5.0', 'ErrorControlChainMiddleware is deprecated and will be removed completely');
} }
/** /**
@ -50,6 +63,10 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
public function process(HTTPRequest $request, callable $next) public function process(HTTPRequest $request, callable $next)
{ {
if (!$this->legacy) {
return call_user_func($next, $request);
}
$result = null; $result = null;
// Prepare tokens and execute chain // Prepare tokens and execute chain

View File

@ -14,6 +14,8 @@ use SilverStripe\Security\Security;
* Specialised Director class used by ErrorControlChain to handle error and redirect conditions * Specialised Director class used by ErrorControlChain to handle error and redirect conditions
* *
* @internal This class is experimental API and may change without warning * @internal This class is experimental API and may change without warning
*
* @deprecated 5.0 To be removed in SilverStripe 5.0
*/ */
class ErrorDirector extends Director class ErrorDirector extends Director
{ {

View File

@ -0,0 +1,20 @@
<?php
namespace SilverStripe\Core\Startup;
/**
* Public interface for startup flush discoverers
*/
interface FlushDiscoverer
{
/**
* Check whether we have to flush manifest
*
* The return value is either null or a bool
* - null means the discoverer does not override the default behaviour (other discoverers decision)
* - bool means the discoverer wants to force flush or prevent it (true or false respectively)
*
* @return null|bool null if don't care or bool to force or prevent flush
*/
public function shouldFlush();
}

View File

@ -15,6 +15,8 @@ use SilverStripe\Security\RandomGenerator;
* redirected URL * redirected URL
* *
* @internal This class is designed specifically for use pre-startup and may change without warning * @internal This class is designed specifically for use pre-startup and may change without warning
*
* @deprecated 5.0 To be removed in SilverStripe 5.0
*/ */
class ParameterConfirmationToken extends AbstractConfirmationToken class ParameterConfirmationToken extends AbstractConfirmationToken
{ {

View File

@ -0,0 +1,88 @@
<?php
namespace SilverStripe\Core\Startup;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Environment;
/**
* The default flush discovery implementation
*
* - if request has `flush` or URL is `dev/build`
* - AND in CLI or DEV mode
* - then flush
*/
class RequestFlushDiscoverer implements FlushDiscoverer
{
/**
* Environment type (dev, test or live)
*
* @var string
*/
protected $env;
/**
* Active request instance (session is not initialized yet!)
*
* @var HTTPRequest
*/
protected $request;
/**
* Initialize it with active Request and Kernel
*
* @param HTTPRequest $request instance of the request (session is not initialized yet!)
* @param string $env Environment type (dev, test or live)
*/
public function __construct(HTTPRequest $request, $env)
{
$this->env = $env;
$this->request = $request;
}
/**
* Checks whether the request contains any flush indicators
*
* @param HTTPRequest $request active request
*
* @return null|bool flush or don't care
*/
protected function lookupRequest()
{
$request = $this->request;
$getVar = array_key_exists('flush', $request->getVars());
$devBuild = $request->getURL() === 'dev/build';
// WARNING!
// We specifically return `null` and not `false` here so that
// it does not override other FlushDiscoverers
return ($getVar || $devBuild) ? true : null;
}
/**
* Checks for permission to flush
*
* Startup flush through a request is only allowed
* to CLI or DEV modes for security reasons
*
* @return bool|null true for allow, false for denying, or null if don't care
*/
protected function isAllowed()
{
// WARNING!
// We specifically return `null` and not `false` here so that
// it does not override other FlushDiscoverers
return (Environment::isCli() || $this->env === Kernel::DEV) ? true : null;
}
public function shouldFlush()
{
if (!$allowed = $this->isAllowed()) {
return $allowed;
}
return $this->lookupRequest();
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace SilverStripe\Core\Startup;
use SilverStripe\Core\Kernel;
/**
* Checks the manifest cache for flush being scheduled in a
* previous request
*/
class ScheduledFlushDiscoverer implements FlushDiscoverer
{
/**
* Active kernel
*
* @var Kernel
*/
protected $kernel;
public function __construct(Kernel $kernel)
{
$this->kernel = $kernel;
}
/**
* Returns the flag whether the manifest flush
* has been scheduled in previous requests
*
* @return bool unix timestamp
*/
protected function getFlush()
{
$classLoader = $this->kernel->getClassLoader();
$classManifest = $classLoader->getManifest();
return (bool) $classManifest->isFlushScheduled();
}
/**
* @internal This method is not a part of public API and will be deleted without a deprecation warning
*
* This method is here so that scheduleFlush functionality implementation is kept close to the check
* implementation.
*/
public static function scheduleFlush(Kernel $kernel)
{
$classLoader = $kernel->getClassLoader();
$classManifest = $classLoader->getManifest();
if (!$classManifest->isFlushScheduled()) {
$classManifest->scheduleFlush();
return true;
}
return false;
}
public function shouldFlush()
{
if ($this->getFlush()) {
return true;
}
return null;
}
}

View File

@ -11,6 +11,8 @@ use SilverStripe\Control\HTTPRequest;
* by generating a one-time-use token & redirecting with that token included in the redirected URL * 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 * @internal This class is designed specifically for use pre-startup and may change without warning
*
* @deprecated 5.0 To be removed in SilverStripe 5.0
*/ */
class URLConfirmationToken extends AbstractConfirmationToken class URLConfirmationToken extends AbstractConfirmationToken
{ {

View File

@ -0,0 +1,33 @@
<?php
namespace SilverStripe\Dev;
use SilverStripe\Control\Director;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\Security\Confirmation;
/**
* A simple controller using DebugView to wrap up the confirmation form
* with a template similar to other DevelopmentAdmin endpoints and UIs
*
* This is done particularly for the confirmation of URL special parameters
* and /dev/build, so that people opening the confirmation form wouldn't
* mix it up with some non-dev functionality
*/
class DevConfirmationController extends Confirmation\Handler
{
public function index()
{
$response = parent::index();
$renderer = DebugView::create();
echo $renderer->renderHeader();
echo $renderer->renderInfo(
_t(__CLASS__.".INFO_TITLE", "Security Confirmation"),
Director::absoluteBaseURL(),
_t(__CLASS__.".INFO_DESCRIPTION", "Confirm potentially dangerous operation")
);
return $response;
}
}

View File

@ -52,10 +52,22 @@ class DevelopmentAdmin extends Controller
*/ */
private static $allow_all_cli = true; private static $allow_all_cli = true;
/**
* Deny all non-cli requests (browser based ones) to dev admin
*
* @config
* @var bool
*/
private static $deny_non_cli = false;
protected function init() protected function init()
{ {
parent::init(); parent::init();
if (static::config()->get('deny_non_cli') && !Director::is_cli()) {
return $this->httpError(404);
}
// Special case for dev/build: Defer permission checks to DatabaseAdmin->init() (see #4957) // Special case for dev/build: Defer permission checks to DatabaseAdmin->init() (see #4957)
$requestedDevBuild = (stripos($this->getRequest()->getURL(), 'dev/build') === 0) $requestedDevBuild = (stripos($this->getRequest()->getURL(), 'dev/build') === 0)
&& (stripos($this->getRequest()->getURL(), 'dev/build/defaults') === false); && (stripos($this->getRequest()->getURL(), 'dev/build/defaults') === false);

View File

@ -13,10 +13,10 @@ use SilverStripe\Core\CoreKernel;
use SilverStripe\Core\EnvironmentLoader; use SilverStripe\Core\EnvironmentLoader;
use SilverStripe\Core\Kernel; use SilverStripe\Core\Kernel;
use SilverStripe\Core\Path; use SilverStripe\Core\Path;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\ORM\DatabaseAdmin; use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\Security\DefaultAdminService; use SilverStripe\Security\DefaultAdminService;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Control\Middleware\URLSpecialsMiddleware\SessionEnvTypeSwitcher;
/** /**
* This installer doesn't use any of the fancy SilverStripe stuff in case it's unsupported. * This installer doesn't use any of the fancy SilverStripe stuff in case it's unsupported.
@ -24,6 +24,7 @@ use SilverStripe\Security\Security;
class Installer class Installer
{ {
use InstallEnvironmentAware; use InstallEnvironmentAware;
use SessionEnvTypeSwitcher;
/** /**
* Errors during install * Errors during install
@ -203,12 +204,19 @@ PHP
// Check result of install // Check result of install
if (!$this->errors) { if (!$this->errors) {
// switch the session to Dev mode so that
// flush does not require authentication
// for the first time after installation
$request['isDev'] = '1';
$this->setSessionEnvType($request);
unset($request['isDev']);
$request->getSession()->save($request);
if (isset($_SERVER['HTTP_HOST']) && $this->hasRewritingCapability()) { if (isset($_SERVER['HTTP_HOST']) && $this->hasRewritingCapability()) {
$this->statusMessage("Checking that friendly URLs work..."); $this->statusMessage("Checking that friendly URLs work...");
$this->checkRewrite(); $this->checkRewrite();
} else { } else {
$token = new ParameterConfirmationToken('flush', $request); $params = http_build_query($request->getVars() + ['flush' => '']);
$params = http_build_query($token->params());
$destinationURL = 'index.php/' . $destinationURL = 'index.php/' .
($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params"); ($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
@ -247,7 +255,6 @@ HTML;
use SilverStripe\Control\HTTPApplication; use SilverStripe\Control\HTTPApplication;
use SilverStripe\Control\HTTPRequestBuilder; use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Core\CoreKernel; use SilverStripe\Core\CoreKernel;
use SilverStripe\Core\Startup\ErrorControlChainMiddleware;
// Find autoload.php // Find autoload.php
if (file_exists(__DIR__ . '/vendor/autoload.php')) { if (file_exists(__DIR__ . '/vendor/autoload.php')) {
@ -265,7 +272,6 @@ $request = HTTPRequestBuilder::createFromEnvironment();
// Default application // Default application
$kernel = new CoreKernel(BASE_PATH); $kernel = new CoreKernel(BASE_PATH);
$app = new HTTPApplication($kernel); $app = new HTTPApplication($kernel);
$app->addMiddleware(new ErrorControlChainMiddleware($app));
$response = $app->handle($request); $response = $app->handle($request);
$response->output(); $response->output();
PHP; PHP;
@ -598,8 +604,7 @@ TEXT;
public function checkRewrite() public function checkRewrite()
{ {
$token = new ParameterConfirmationToken('flush', new HTTPRequest('GET', '/')); $params = http_build_query(['flush' => '']);
$params = http_build_query($token->params());
$destinationURL = rtrim(BASE_URL, '/') . '/' . ( $destinationURL = rtrim(BASE_URL, '/') . '/' . (
$this->checkModuleExists('cms') $this->checkModuleExists('cms')

View File

@ -0,0 +1,171 @@
<?php
namespace SilverStripe\Security\Confirmation;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\Form as BaseForm;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\FieldGroup;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LabelField;
use SilverStripe\ORM\ValidationException;
/**
* Basic confirmation form implementation.
*
* Renders the list of confirmation items on the screen
* and reconciles those with the confirmation storage.
*
* If the user confirms the action, marks the storage as confirmed
* and redirects to the success url (kept in the storage).
*
* If the user declines the action, cleans up the storage and
* redirects to the failure url (kept in the storage).
*/
class Form extends BaseForm
{
/**
* Confirmation storage instance
*
* @var Storage
*/
private $storage;
/**
* @param string $storageId confirmation storage identifier
* @param RequestHandler $controller active request handler
* @param string $formConstructor form constructor name
*/
public function __construct($storageId, RequestHandler $controller, $formConstructor)
{
$request = $controller->getRequest();
$storage = Injector::inst()->createWithArgs(Storage::class, [$request->getSession(), $storageId, false]);
if (count($storage->getItems())) {
$fieldList = $this->buildFieldList($storage);
$actionList = $this->buildActionList($storage);
} else {
$fieldList = $this->buildEmptyFieldList();
$actionList = null;
}
parent::__construct($controller, $formConstructor, $fieldList, $actionList);
if ($storage->getHttpMethod() !== 'POST') {
$this->enableSecurityToken();
}
$this->storage = $storage;
}
/**
* The form refusal handler. Cleans up the confirmation storage
* and returns the failure redirection (kept in the storage)
*
* @return HTTPResponse redirect
*/
public function doRefuse()
{
$url = $this->storage->getFailureUrl();
$this->storage->cleanup();
return $this->controller->redirect($url);
}
/**
* The form confirmation handler. Checks all the items in the storage
* has been confirmed and marks them as such. Returns a redirect
* when all the storage items has been verified and marked as confirmed.
*
* @return HTTPResponse success url
*
* @throws ValidationException when the confirmation storage has an item missing on the form
*/
public function doConfirm()
{
$storage = $this->storage;
$data = $this->getData();
if (!$storage->confirm($data)) {
throw new ValidationException('Sorry, we could not verify the parameters');
}
$url = $storage->getSuccessUrl();
return $this->controller->redirect($url);
}
protected function buildActionList(Storage $storage)
{
$cancel = FormAction::create('doRefuse', _t(__CLASS__.'.REFUSE', 'Cancel'));
$confirm = FormAction::create('doConfirm', _t(__CLASS__.'.CONFIRM', 'Run the action'))->setAutofocus(true);
if ($storage->getHttpMethod() === 'POST') {
$confirm->setAttribute('formaction', htmlspecialchars($storage->getSuccessUrl()));
}
return FieldList::create($cancel, $confirm);
}
/**
* Builds the form fields taking the confirmation items from the storage
*
* @param Storage $storage Confirmation storage instance
*
* @return FieldList
*/
protected function buildFieldList(Storage $storage)
{
$fields = [];
foreach ($storage->getItems() as $item) {
$group = [];
$group[] = HeaderField::create(null, $item->getName());
if ($item->getDescription()) {
$group[] = LabelField::create($item->getDescription());
}
$fields[] = FieldGroup::create(...$group);
}
foreach ($storage->getHashedItems() as $key => $val) {
$fields[] = HiddenField::create($key, null, $val);
}
if ($storage->getHttpMethod() === 'POST') {
// add the storage CSRF token
$fields[] = HiddenField::create($storage->getCsrfToken(), null, '1');
// replicate the original POST request parameters
// so that the new confirmed POST request has those
$data = $storage->getSuccessPostVars();
if (is_null($data)) {
throw new ValidationException('Sorry, your cookies seem to have expired. Try to repeat the initial action.');
}
foreach ($data as $key => $value) {
$fields[] = HiddenField::create($key, null, $value);
}
}
return FieldList::create(...$fields);
}
/**
* Builds the fields showing the form is empty and there's nothing
* to confirm
*
* @return FieldList
*/
protected function buildEmptyFieldList()
{
return FieldList::create(
HeaderField::create(null, _t(__CLASS__.'.EMPTY_TITLE', 'Nothing to confirm'))
);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace SilverStripe\Security\Confirmation;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\Form as BaseForm;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
/**
* Confirmation form handler implementation
*
* Handles StorageID identifier in the URL
*/
class Handler extends RequestHandler
{
private static $url_handlers = [
'$StorageID!/$Action//$ID/$OtherID' => '$Action',
];
private static $allowed_actions = [
'index',
'Form'
];
public function Link($action = null)
{
$request = Injector::inst()->get(HTTPRequest::class);
$link = Controller::join_links(Director::baseURL(), $request->getUrl(), $action);
$this->extend('updateLink', $link, $action);
return $link;
}
/**
* URL handler for the log-in screen
*
* @return array
*/
public function index()
{
return [
'Title' => _t(__CLASS__.'.FORM_TITLE', 'Confirm potentially dangerous action'),
'Form' => $this->Form()
];
return $this;
}
/**
* This method is being used by Form to check whether it needs to use SecurityToken
*
* We always return false here as the confirmation form should decide this on its own
* depending on the Storage data. If we had the original request to
* be POST with its own SecurityID, we don't want to interfre with it. If it's been
* GET request, then it will generate a new SecurityToken
*
* @return bool
*/
public function securityTokenEnabled()
{
return false;
}
/**
* Returns an instance of Confirmation\Form initialized
* with the proper storage id taken from URL
*
* @return Form
*/
public function Form()
{
$storageId = $this->request->param('StorageID');
if (!strlen(trim($storageId))) {
$this->httpError(404, "Undefined StorageID");
}
return Form::create($storageId, $this, __FUNCTION__);
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace SilverStripe\Security\Confirmation;
/**
* Confirmation item is a simple data object
* incapsulating a single confirmation unit,
* its unique identifier (token), its human
* friendly name, description and the status
* whether it has already been confirmed.
*/
class Item
{
/**
* A confirmation token which is
* unique for every confirmation item
*
* @var string
*/
private $token;
/**
* Human readable item name
*
* @var string
*/
private $name;
/**
* Human readable description of the item
*
* @var string
*/
private $description;
/**
* Whether the item has been confirmed or not
*
* @var bool
*/
private $confirmed;
/**
* @param string $token unique token of this confirmation item
* @param string $name Human readable name of the item
* @param string $description Human readable description of the item
*/
public function __construct($token, $name, $description)
{
$this->token = $token;
$this->name = $name;
$this->description = $description;
$this->confirmed = false;
}
/**
* Returns the token of the item
*
* @return string
*/
public function getToken()
{
return $this->token;
}
/**
* Returns the item name (human readable)
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Returns the human readable description of the item
*
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Returns whether the item has been confirmed
*
* @return bool
*/
public function isConfirmed()
{
return $this->confirmed;
}
/**
* Mark the item as confirmed
*/
public function confirm()
{
$this->confirmed = true;
}
}

View File

@ -0,0 +1,444 @@
<?php
namespace SilverStripe\Security\Confirmation;
use SilverStripe\Control\Cookie;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Security\SecurityToken;
/**
* Confirmation Storage implemented on top of SilverStripe Session and Cookie
*
* The storage keeps the information about the items requiring
* confirmation and their status (confirmed or not) in Session
*
* User data, such as the original request parameters, may be kept in
* Cookie so that session storage cannot be exhausted easily by a malicious user
*/
class Storage
{
const HASH_ALGO = 'sha512';
/**
* @var \SilverStripe\Control\Session
*/
protected $session;
/**
* Identifier of the storage within the session
*
* @var string
*/
protected $id;
/**
* @param Session $session active session
* @param string $id Unique storage identifier within the session
* @param bool $new Cleanup the storage
*/
public function __construct(Session $session, $id, $new = true)
{
$id = trim((string) $id);
if (!strlen($id)) {
throw new \InvalidArgumentException('Storage ID must not be empty');
}
$this->session = $session;
$this->id = $id;
if ($new) {
$this->cleanup();
}
}
/**
* Remove all the data from the storage
* Cleans up Session and Cookie related to this storage
*/
public function cleanup()
{
Cookie::force_expiry($this->getCookieKey());
$this->session->clear($this->getNamespace());
}
/**
* Gets user input data (usually POST array), checks all the items in the storage
* has been confirmed and marks them as such.
*
* @param array $data User input to look at for items. Usually POST array
*
* @return bool whether all items have been confirmed
*/
public function confirm($data)
{
foreach ($this->getItems() as $item) {
$key = base64_encode($this->getTokenHash($item));
if (!isset($data[$key]) || $data[$key] !== '1') {
return false;
}
$item->confirm();
$this->putItem($item);
}
return true;
}
/**
* Returns the dictionary with the item hashes
*
* The {@see SilverStripe\Security\Confirmation\Storage::confirm} function
* expects exactly same dictionary as its argument for successful confirmation
*
* Keys of the dictionary are salted item token hashes
* All values are the string "1" constantly
*
* @return array
*/
public function getHashedItems()
{
$items = [];
foreach ($this->getItems() as $item) {
$hash = base64_encode($this->getTokenHash($item));
$items[$hash] = '1';
}
return $items;
}
/**
* Returns salted and hashed version of the item token
*
* @param Item $item
*
* @return string
*/
public function getTokenHash(Item $item)
{
$token = $item->getToken();
$salt = $this->getSessionSalt();
$salted = $salt.$token;
return hash(static::HASH_ALGO, $salted, true);
}
/**
* Returns the unique cookie key generated from the session salt
*
* @return string
*/
public function getCookieKey()
{
$salt = $this->getSessionSalt();
return bin2hex(hash(static::HASH_ALGO, $salt.'cookie key', true));
}
/**
* Returns a unique token to use as a CSRF token
*
* @return string
*/
public function getCsrfToken()
{
$salt = $this->getSessionSalt();
return base64_encode(hash(static::HASH_ALGO, $salt.'csrf token', true));
}
/**
* Returns the salt generated for the current session
*
* @return string
*/
public function getSessionSalt()
{
$key = $this->getNamespace('salt');
if (!$salt = $this->session->get($key)) {
$salt = $this->generateSalt();
$this->session->set($key, $salt);
}
return $salt;
}
/**
* Returns randomly generated salt
*
* @return string
*/
protected function generateSalt()
{
return random_bytes(64);
}
/**
* Adds a new object to the list of confirmation items
* Replaces the item if there is already one with the same token
*
* @param Item $item Item requiring confirmation
*
* @return $this
*/
public function putItem(Item $item)
{
$key = $this->getNamespace('items');
$items = $this->session->get($key) ?: [];
$token = $this->getTokenHash($item);
$items[$token] = $item;
$this->session->set($key, $items);
return $this;
}
/**
* Returns the list of registered confirmation items
*
* @return Item[]
*/
public function getItems()
{
return $this->session->get($this->getNamespace('items')) ?: [];
}
/**
* Look up an item by its token key
*
* @param string $key Item token key
*
* @return null|Item
*/
public function getItem($key)
{
foreach ($this->getItems() as $item) {
if ($item->getToken() === $key) {
return $item;
}
}
}
/**
* This request should be performed on success
* Usually the original request which triggered the confirmation
*
* @param HTTPRequest $request
*
* @return $this
*/
public function setSuccessRequest(HTTPRequest $request)
{
$this->setSuccessUrl($request->getURL(true));
$httpMethod = $request->httpMethod();
$this->session->set($this->getNamespace('httpMethod'), $httpMethod);
if ($httpMethod === 'POST') {
$checksum = $this->setSuccessPostVars($request->postVars());
$this->session->set($this->getNamespace('postChecksum'), $checksum);
}
}
/**
* Save the post data in the storage (browser Cookies by default)
* Returns the control checksum of the data preserved
*
* Keeps data in Cookies to avoid potential DDoS targeting
* session storage exhaustion
*
* @param array $data
*
* @return string checksum
*/
protected function setSuccessPostVars(array $data)
{
$checksum = hash_init(static::HASH_ALGO);
$cookieData = [];
foreach ($data as $key => $val) {
$key = strval($key);
$val = strval($val);
hash_update($checksum, $key);
hash_update($checksum, $val);
$cookieData[] = [$key, $val];
}
$checksum = hash_final($checksum, true);
$cookieData = json_encode($cookieData, 0, 2);
$cookieKey = $this->getCookieKey();
Cookie::set($cookieKey, $cookieData, 0);
return $checksum;
}
/**
* Returns HTTP method of the success request
*
* @return string
*/
public function getHttpMethod()
{
return $this->session->get($this->getNamespace('httpMethod'));
}
/**
* Returns the list of success request post parameters
*
* Returns null if no parameters was persisted initially or
* if the checksum is incorrect.
*
* WARNING! If HTTP Method is POST and this function returns null,
* you MUST assume the Cookie parameter either has been forged or
* expired.
*
* @return array|null
*/
public function getSuccessPostVars()
{
$controlChecksum = $this->session->get($this->getNamespace('postChecksum'));
if (!$controlChecksum) {
return null;
}
$cookieKey = $this->getCookieKey();
$cookieData = Cookie::get($cookieKey);
if (!$cookieData) {
return null;
}
$cookieData = json_decode($cookieData, true, 3);
if (!is_array($cookieData)) {
return null;
}
$checksum = hash_init(static::HASH_ALGO);
$data = [];
foreach ($cookieData as $pair) {
if (!isset($pair[0]) || !isset($pair[1])) {
return null;
}
$key = $pair[0];
$val = $pair[1];
hash_update($checksum, $key);
hash_update($checksum, $val);
$data[$key] = $val;
}
$checksum = hash_final($checksum, true);
if ($checksum !== $controlChecksum) {
return null;
}
return $data;
}
/**
* The URL the form should redirect to on success
*
* @param string $url Success URL
*
* @return $this;
*/
public function setSuccessUrl($url)
{
$this->session->set($this->getNamespace('successUrl'), $url);
return $this;
}
/**
* Returns the URL registered by {@see self::setSuccessUrl} as a success redirect target
*
* @return string
*/
public function getSuccessUrl()
{
return $this->session->get($this->getNamespace('successUrl'));
}
/**
* The URL the form should redirect to on failure
*
* @param string $url Failure URL
*
* @return $this;
*/
public function setFailureUrl($url)
{
$this->session->set($this->getNamespace('failureUrl'), $url);
return $this;
}
/**
* Returns the URL registered by {@see self::setFailureUrl} as a success redirect target
*
* @return string
*/
public function getFailureUrl()
{
return $this->session->get($this->getNamespace('failureUrl'));
}
/**
* Check all items to be confirmed in the storage
*
* @param Item[] $items List of items to be checked
*
* @return bool
*/
public function check(array $items)
{
foreach ($items as $itemToConfirm) {
foreach ($this->getItems() as $item) {
if ($item->getToken() !== $itemToConfirm->getToken()) {
continue;
}
if ($item->isConfirmed()) {
continue 2;
}
break;
}
return false;
}
return true;
}
/**
* Returns the namespace of the storage in the session
*
* @param string|null $key Optional key within the storage
*
* @return string
*/
protected function getNamespace($key = null)
{
return sprintf(
'%s.%s%s',
str_replace('\\', '.', __CLASS__),
$this->id,
$key ? '.'.$key : ''
);
}
}

View File

@ -37,13 +37,13 @@ class Security extends Controller implements TemplateGlobalProvider
{ {
private static $allowed_actions = array( private static $allowed_actions = array(
'basicauthlogin',
'changepassword',
'index', 'index',
'login', 'login',
'logout', 'logout',
'basicauthlogin',
'lostpassword', 'lostpassword',
'passwordsent', 'passwordsent',
'changepassword',
'ping', 'ping',
); );
@ -661,7 +661,6 @@ class Security extends Controller implements TemplateGlobalProvider
->clear("Security.Message"); ->clear("Security.Message");
} }
/** /**
* Show the "login" page * Show the "login" page
* *

View File

@ -14,6 +14,7 @@ use SilverStripe\Control\RequestProcessor;
use SilverStripe\Control\Tests\DirectorTest\TestController; use SilverStripe\Control\Tests\DirectorTest\TestController;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Kernel; use SilverStripe\Core\Kernel;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -26,11 +27,15 @@ class DirectorTest extends SapphireTest
TestController::class, TestController::class,
]; ];
private $originalEnvType;
protected function setUp() protected function setUp()
{ {
parent::setUp(); parent::setUp();
Director::config()->set('alternate_base_url', 'http://www.mysite.com:9090/'); Director::config()->set('alternate_base_url', 'http://www.mysite.com:9090/');
$this->originalEnvType = Environment::getEnv('SS_ENVIRONMENT_TYPE');
// Ensure redirects enabled on all environments and global state doesn't affect the tests // Ensure redirects enabled on all environments and global state doesn't affect the tests
CanonicalURLMiddleware::singleton() CanonicalURLMiddleware::singleton()
->setForceSSLDomain(null) ->setForceSSLDomain(null)
@ -39,6 +44,12 @@ class DirectorTest extends SapphireTest
$this->expectedRedirect = null; $this->expectedRedirect = null;
} }
protected function tearDown(...$args)
{
Environment::setEnv('SS_ENVIRONMENT_TYPE', $this->originalEnvType);
parent::tearDown(...$args);
}
protected function getExtraRoutes() protected function getExtraRoutes()
{ {
$rules = parent::getExtraRoutes(); $rules = parent::getExtraRoutes();
@ -406,7 +417,7 @@ class DirectorTest extends SapphireTest
} }
/** /**
* Tests isDev, isTest, isLive set from querystring * Tests isDev, isTest, isLive cannot be set from querystring
*/ */
public function testQueryIsEnvironment() public function testQueryIsEnvironment()
{ {
@ -422,30 +433,33 @@ class DirectorTest extends SapphireTest
/** @var Kernel $kernel */ /** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class); $kernel = Injector::inst()->get(Kernel::class);
$kernel->setEnvironment(null); $kernel->setEnvironment(null);
Environment::setEnv('SS_ENVIRONMENT_TYPE', Kernel::LIVE);
$this->assertTrue(Director::isLive());
// Test isDev=1 // Test isDev=1
$_GET['isDev'] = '1'; $_GET['isDev'] = '1';
$this->assertTrue(Director::isDev()); $this->assertFalse(Director::isDev());
$this->assertFalse(Director::isTest()); $this->assertFalse(Director::isTest());
$this->assertFalse(Director::isLive()); $this->assertTrue(Director::isLive());
// Test persistence // Test persistence
unset($_GET['isDev']); unset($_GET['isDev']);
$this->assertTrue(Director::isDev()); $this->assertFalse(Director::isDev());
$this->assertFalse(Director::isTest()); $this->assertFalse(Director::isTest());
$this->assertFalse(Director::isLive()); $this->assertTrue(Director::isLive());
// Test change to isTest // Test change to isTest
$_GET['isTest'] = '1'; $_GET['isTest'] = '1';
$this->assertFalse(Director::isDev()); $this->assertFalse(Director::isDev());
$this->assertTrue(Director::isTest()); $this->assertFalse(Director::isTest());
$this->assertFalse(Director::isLive()); $this->assertTrue(Director::isLive());
// Test persistence // Test persistence
unset($_GET['isTest']); unset($_GET['isTest']);
$this->assertFalse(Director::isDev()); $this->assertFalse(Director::isDev());
$this->assertTrue(Director::isTest()); $this->assertFalse(Director::isTest());
$this->assertFalse(Director::isLive()); $this->assertTrue(Director::isLive());
} }
public function testResetGlobalsAfterTestRequest() public function testResetGlobalsAfterTestRequest()

View File

@ -2,6 +2,8 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Control\Tests\FlushMiddlewareTest\TestFlushable; use SilverStripe\Control\Tests\FlushMiddlewareTest\TestFlushable;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
@ -13,7 +15,12 @@ class FlushMiddlewareTest extends FunctionalTest
public function testImplementorsAreCalled() public function testImplementorsAreCalled()
{ {
TestFlushable::$flushed = false; TestFlushable::$flushed = false;
$this->get('?flush=1');
Injector::inst()->get(Kernel::class)->boot(true);
$this->get('/');
$this->assertTrue(TestFlushable::$flushed); $this->assertTrue(TestFlushable::$flushed);
// reset the kernel Flush flag
Injector::inst()->get(Kernel::class)->boot();
} }
} }

View File

@ -0,0 +1,57 @@
<?php
namespace SilverStripe\Control\Tests;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
trait HttpRequestMockBuilder
{
/**
* Builds and returns a new mock instance of HTTPRequest
*
* @param string $url
* @param array $getVars GET parameters
* @param array $postVars POST parameters
* @param string|null $method HTTP method
* @param Session|null $session Session instance
*
* @return HTTPRequest
*/
public function buildRequestMock($url, $getVars = [], $postVars = [], $method = null, Session $session = null)
{
if (is_null($session)) {
$session = new Session([]);
}
$request = $this->createMock(HTTPRequest::class);
$request->method('getSession')->willReturn($session);
$request->method('getURL')->will($this->returnCallback(static function ($addParams) use ($url, $getVars) {
return $addParams && count($getVars) ? $url.'?'.http_build_query($getVars) : $url;
}));
$request->method('getVars')->willReturn($getVars);
$request->method('getVar')->will($this->returnCallback(static function ($key) use ($getVars) {
return isset($getVars[$key]) ? $getVars[$key] : null;
}));
$request->method('postVars')->willReturn($postVars);
$request->method('postVar')->will($this->returnCallback(static function ($key) use ($postVars) {
return isset($postVars[$key]) ? $postVars[$key] : null;
}));
if (is_null($method)) {
if (count($postVars)) {
$method = 'POST';
} else {
$method = 'GET';
}
}
$request->method('httpMethod')->willReturn($method);
return $request;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace SilverStripe\Control\Tests\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\AjaxBypass;
use SilverStripe\Dev\SapphireTest;
class AjaxBypassTest extends SapphireTest
{
public function testBypass()
{
$ajaxRequest = $this->createMock(HTTPRequest::class);
$ajaxRequest->method('isAjax')->willReturn(true);
$simpleRequest = $this->createMock(HTTPRequest::class);
$simpleRequest->method('isAjax')->willReturn(false);
$ajaxBypass = new AjaxBypass();
$this->assertFalse($ajaxBypass->checkRequestForBypass($simpleRequest));
$this->assertTrue($ajaxBypass->checkRequestForBypass($ajaxRequest));
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace SilverStripe\Control\Tests\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\GetParameter;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Confirmation\Item;
class GetParameterTest extends SapphireTest
{
use HttpRequestMockBuilder;
public function testName()
{
$rule = new GetParameter('name_01');
$this->assertEquals('name_01', $rule->getName());
$rule->setName('name_02');
$this->assertEquals('name_02', $rule->getName());
}
public function testBypass()
{
$request = $this->buildRequestMock('test/path', ['parameterKey' => 'parameterValue']);
$rule = new GetParameter('parameterKey_01');
$this->assertFalse($rule->checkRequestForBypass($request));
$rule->setName('parameterKey');
$this->assertTrue($rule->checkRequestForBypass($request));
}
public function testConfirmationItem()
{
$request = $this->buildRequestMock('test/path', ['parameterKey' => 'parameterValue']);
$rule = new GetParameter('parameterKey_01');
$this->assertNull($rule->getRequestConfirmationItem($request));
$rule->setName('parameterKey');
$item = $rule->getRequestConfirmationItem($request);
$this->assertNotNull($item);
$this->assertInstanceOf(Item::class, $item);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace SilverStripe\Control\Tests\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\HttpMethodBypass;
use SilverStripe\Dev\SapphireTest;
class HttpMethodBypassTest extends SapphireTest
{
public function testBypass()
{
$getRequest = $this->createMock(HTTPRequest::class);
$getRequest->method('httpMethod')->willReturn('GET');
$postRequest = $this->createMock(HTTPRequest::class);
$postRequest->method('httpMethod')->willReturn('POST');
$putRequest = $this->createMock(HTTPRequest::class);
$putRequest->method('httpMethod')->willReturn('PUT');
$delRequest = $this->createMock(HTTPRequest::class);
$delRequest->method('httpMethod')->willReturn('DELETE');
$bypass = new HttpMethodBypass('GET', 'POST');
$this->assertTrue($bypass->checkRequestForBypass($getRequest));
$this->assertTrue($bypass->checkRequestForBypass($postRequest));
$this->assertFalse($bypass->checkRequestForBypass($putRequest));
$this->assertFalse($bypass->checkRequestForBypass($delRequest));
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace SilverStripe\Control\Tests\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswithCaseInsensitive;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Confirmation\Item;
class UrlPathStartswithCaseInsensitiveTest extends SapphireTest
{
use HttpRequestMockBuilder;
public function testPath()
{
$url = new UrlPathStartswithCaseInsensitive('test/path_01');
$this->assertEquals('test/path_01/', $url->getPath());
$url->setPath('test/path_02');
$this->assertEquals('test/path_02/', $url->getPath());
}
public function testBypass()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$url = new UrlPathStartswithCaseInsensitive('dev');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswithCaseInsensitive('dev/');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswithCaseInsensitive('dev/build');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswithCaseInsensitive('dev/build/');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswithCaseInsensitive('de');
$this->assertFalse($url->checkRequestForBypass($request));
$url = new UrlPathStartswithCaseInsensitive('dev/buil');
$this->assertFalse($url->checkRequestForBypass($request));
$url = new UrlPathStartswithCaseInsensitive('Dev');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswithCaseInsensitive('dev/builD');
$this->assertTrue($url->checkRequestForBypass($request));
}
public function testConfirmationItem()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$url = new UrlPathStartswithCaseInsensitive('dev');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswithCaseInsensitive('dev/');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswithCaseInsensitive('dev/build');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswithCaseInsensitive('dev/build/');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswithCaseInsensitive('de');
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswithCaseInsensitive('dev/buil');
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswithCaseInsensitive('Dev/build');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswithCaseInsensitive('dev/builD');
$this->assertNotNull($url->getRequestConfirmationItem($request));
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace SilverStripe\Control\Tests\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\UrlPathStartswith;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Confirmation\Item;
class UrlPathStartswithTest extends SapphireTest
{
use HttpRequestMockBuilder;
public function testPath()
{
$url = new UrlPathStartswith('test/path_01');
$this->assertEquals('test/path_01/', $url->getPath());
$url->setPath('test/path_02');
$this->assertEquals('test/path_02/', $url->getPath());
}
public function testBypass()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$url = new UrlPathStartswith('dev');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswith('dev/');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswith('dev/build');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswith('dev/build/');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new UrlPathStartswith('de');
$this->assertFalse($url->checkRequestForBypass($request));
$url = new UrlPathStartswith('dev/buil');
$this->assertFalse($url->checkRequestForBypass($request));
$url = new UrlPathStartswith('Dev');
$this->assertFalse($url->checkRequestForBypass($request));
$url = new UrlPathStartswith('dev/builD');
$this->assertFalse($url->checkRequestForBypass($request));
}
public function testConfirmationItem()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$url = new UrlPathStartswith('dev');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswith('dev/');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswith('dev/build');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswith('dev/build/');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswith('de');
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswith('dev/buil');
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswith('Dev/build');
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new UrlPathStartswith('dev/builD');
$this->assertNull($url->getRequestConfirmationItem($request));
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace SilverStripe\Control\Tests\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\Url;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Confirmation\Item;
class UrlTest extends SapphireTest
{
use HttpRequestMockBuilder;
public function testPath()
{
$url = new Url('test/path_01');
$this->assertEquals('test/path_01/', $url->getPath());
$url->setPath('test/path_02');
$this->assertEquals('test/path_02/', $url->getPath());
}
public function testHttpMethods()
{
$url = new Url('/', ['PUT', 'DELETE']);
$this->assertCount(2, $url->getHttpMethods());
$this->assertContains('DELETE', $url->getHttpMethods());
$this->assertContains('PUT', $url->getHttpMethods());
$url->addHttpMethods('GET', 'POST');
$this->assertCount(4, $url->getHttpMethods());
$this->assertContains('DELETE', $url->getHttpMethods());
$this->assertContains('GET', $url->getHttpMethods());
$this->assertContains('POST', $url->getHttpMethods());
$this->assertContains('PUT', $url->getHttpMethods());
}
public function testBypass()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$url = new Url('dev');
$this->assertFalse($url->checkRequestForBypass($request));
$url = new Url('dev/build');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new Url('dev/build/');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new Url('dev/build', 'GET');
$this->assertTrue($url->checkRequestForBypass($request));
$url = new Url('dev/build', 'POST');
$this->assertFalse($url->checkRequestForBypass($request));
$url = new Url('dev/build', ['GET', 'POST']);
$this->assertTrue($url->checkRequestForBypass($request));
$url = new Url('dev/build', null, []);
$this->assertFalse($url->checkRequestForBypass($request));
$url = new Url('dev/build', null, ['flush' => null]);
$this->assertTrue($url->checkRequestForBypass($request));
$url = new Url('dev/build', null, ['flush' => '1']);
$this->assertFalse($url->checkRequestForBypass($request));
$url = new Url('dev/build', null, ['flush' => 'all']);
$this->assertTrue($url->checkRequestForBypass($request));
}
public function testConfirmationItem()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$url = new Url('dev');
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build/');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build', 'GET');
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build', 'POST');
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build', ['GET', 'POST']);
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build', null, []);
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build', null, ['flush' => null]);
$this->assertNotNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build', null, ['flush' => '1']);
$this->assertNull($url->getRequestConfirmationItem($request));
$url = new Url('dev/build', null, ['flush' => 'all']);
$this->assertNotNull($url->getRequestConfirmationItem($request));
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace SilverStripe\Control\Tests\Middleware;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Middleware\ConfirmationMiddleware;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\Url;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
use SilverStripe\Dev\SapphireTest;
class ConfirmationMiddlewareTest extends SapphireTest
{
use HttpRequestMockBuilder;
public function testBypass()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$middleware = new ConfirmationMiddleware(new Url('dev/build'));
$this->assertFalse($middleware->canBypass($request));
$middleware->setBypasses([new Url('no-match')]);
$this->assertFalse($middleware->canBypass($request));
$middleware->setBypasses([new Url('dev/build')]);
$this->assertTrue($middleware->canBypass($request));
}
public function testConfirmationItems()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$middleware = new ConfirmationMiddleware(
new Url('dev/build'),
new Url('dev/build', null, ['flush' => null])
);
$items = $middleware->getConfirmationItems($request);
$this->assertCount(2, $items);
}
public function testProcess()
{
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
// Testing the middleware does not do anything if rules do not apply
$middleware = new ConfirmationMiddleware(new Url('no-match'));
$next = false;
$middleware->process(
$request,
static function () use (&$next) {
$next = true;
}
);
$this->assertTrue($next);
// Test for a redirection when rules hit the request
$middleware = new ConfirmationMiddleware(new Url('dev/build'));
$next = false;
$response = $middleware->process(
$request,
static function () use (&$next) {
$next = true;
}
);
$this->assertFalse($next);
$this->assertInstanceOf(HTTPResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode());
$this->assertEquals('/dev/confirm/middleware', $response->getHeader('location'));
// Test bypasses have more priority than rules
$middleware->setBypasses([new Url('dev/build')]);
$next = false;
$response = $middleware->process(
$request,
static function () use (&$next) {
$next = true;
}
);
}
}

View File

@ -1,187 +0,0 @@
<?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

@ -1,176 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Startup;
use SilverStripe\Control\HTTPApplication;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Startup\ErrorControlChainMiddleware;
use SilverStripe\Core\Tests\Startup\ErrorControlChainMiddlewareTest\BlankKernel;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Security;
class ErrorControlChainMiddlewareTest extends SapphireTest
{
protected $usesDatabase = true;
protected function setUp()
{
parent::setUp();
Security::force_database_is_ready(true);
}
protected function tearDown()
{
Security::clear_database_is_ready();
parent::tearDown();
}
public function testLiveFlushAdmin()
{
// 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', '/', ['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('flush=1&flushtoken=', $location);
$this->assertNotContains('Security/login', $location);
}
public function testLiveFlushUnauthenticated()
{
// 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', '/', ['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('?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

@ -1,18 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Startup\ErrorControlChainMiddlewareTest;
use SilverStripe\Core\CoreKernel;
class BlankKernel extends CoreKernel
{
public function __construct($basePath)
{
// Noop
}
public function boot($flush = false)
{
// Noop
}
}

View File

@ -1,325 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Startup;
use Exception;
use Foo;
use SilverStripe\Core\Startup\ErrorControlChain;
use SilverStripe\Dev\SapphireTest;
class ErrorControlChainTest extends SapphireTest
{
protected function setUp()
{
// Check we can run PHP at all
$null = is_writeable('/dev/null') ? '/dev/null' : 'NUL';
exec("php -v 2> $null", $out, $rv);
if ($rv != 0) {
$this->markTestSkipped("Can't run PHP from the command line - is it in your path?");
}
parent::setUp();
}
public function testErrorSuppression()
{
// Errors disabled by default
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
$chain->setDisplayErrors('Off'); // mocks display_errors: Off
$initialValue = null;
$whenNotSuppressed = null;
$whenSuppressed = null;
$chain->then(function (ErrorControlChainTest\ErrorControlChainTest_Chain $chain) use (
&$initialValue,
&$whenNotSuppressed,
&$whenSuppressed
) {
$initialValue = $chain->getDisplayErrors();
$chain->setSuppression(false);
$whenNotSuppressed = $chain->getDisplayErrors();
$chain->setSuppression(true);
$whenSuppressed = $chain->getDisplayErrors();
})->execute();
// Disabled errors never un-disable
$this->assertEquals(0, $initialValue); // Chain starts suppressed
$this->assertEquals(0, $whenSuppressed); // false value used internally when suppressed
$this->assertEquals('Off', $whenNotSuppressed); // false value set by php ini when suppression lifted
$this->assertEquals('Off', $chain->getDisplayErrors()); // Correctly restored after run
// Errors enabled by default
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
$chain->setDisplayErrors('Yes'); // non-falsey ini value
$initialValue = null;
$whenNotSuppressed = null;
$whenSuppressed = null;
$chain->then(function (ErrorControlChainTest\ErrorControlChainTest_Chain $chain) use (
&$initialValue,
&$whenNotSuppressed,
&$whenSuppressed
) {
$initialValue = $chain->getDisplayErrors();
$chain->setSuppression(true);
$whenSuppressed = $chain->getDisplayErrors();
$chain->setSuppression(false);
$whenNotSuppressed = $chain->getDisplayErrors();
})->execute();
// Errors can be suppressed an un-suppressed when initially enabled
$this->assertEquals(0, $initialValue); // Chain starts suppressed
$this->assertEquals(0, $whenSuppressed); // false value used internally when suppressed
$this->assertEquals('Yes', $whenNotSuppressed); // false value set by php ini when suppression lifted
$this->assertEquals('Yes', $chain->getDisplayErrors()); // Correctly restored after run
// Fatal error
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
Foo::bar(); // Non-existant class causes fatal error
})
->thenIfErrored(function () {
echo "Done";
})
->executeInSubprocess();
$this->assertEquals('Done', $out);
// User error
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
user_error('Error', E_USER_ERROR);
})
->thenIfErrored(function () {
echo "Done";
})
->executeInSubprocess();
$this->assertEquals('Done', $out);
// Recoverable error
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
$x = function (ErrorControlChain $foo) {
};
$x(1); // Calling against type
})
->thenIfErrored(function () {
echo "Done";
})
->executeInSubprocess();
$this->assertEquals('Done', $out);
// Memory exhaustion
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
ini_set('memory_limit', '10M');
$a = array();
while (1) {
$a[] = 1;
}
})
->thenIfErrored(function () {
echo "Done";
})
->executeInSubprocess();
$this->assertEquals('Done', $out);
// Exceptions
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
throw new Exception("bob");
})
->thenIfErrored(function () {
echo "Done";
})
->executeInSubprocess();
$this->assertEquals('Done', $out);
}
public function testExceptionSuppression()
{
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
throw new Exception('This exception should be suppressed');
})
->thenIfErrored(function () {
echo "Done";
})
->executeInSubprocess();
$this->assertEquals('Done', $out);
}
public function testErrorControl()
{
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
echo 'preThen,';
})
->thenIfErrored(function () {
echo 'preThenIfErrored,';
})
->thenAlways(function () {
echo 'preThenAlways,';
})
->then(function () {
user_error('An error', E_USER_ERROR);
})
->then(function () {
echo 'postThen,';
})
->thenIfErrored(function () {
echo 'postThenIfErrored,';
})
->thenAlways(function () {
echo 'postThenAlways,';
})
->executeInSubprocess();
$this->assertEquals(
"preThen,preThenAlways,postThenIfErrored,postThenAlways,",
$out
);
}
public function testSuppressionControl()
{
// Turning off suppression before execution
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
$chain->setSuppression(false);
list($out, $code) = $chain
->then(function ($chain) {
Foo::bar(); // Non-existant class causes fatal error
})
->executeInSubprocess(true);
$this->assertContains('Fatal error', $out);
$this->assertContains('Foo', $out);
// Turning off suppression during execution
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function ($chain) {
$chain->setSuppression(false);
Foo::bar(); // Non-existent class causes fatal error
})
->executeInSubprocess(true);
$this->assertContains('Fatal error', $out);
$this->assertContains('Foo', $out);
}
public function testDoesntAffectNonFatalErrors()
{
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
$array = null;
if (@$array['key'] !== null) {
user_error('Error', E_USER_ERROR);
}
})
->then(function () {
echo "Good";
})
->thenIfErrored(function () {
echo "Bad";
})
->executeInSubprocess();
$this->assertContains("Good", $out);
}
public function testDoesntAffectCaughtExceptions()
{
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
try {
throw new Exception('Error');
} catch (Exception $e) {
echo "Good";
}
})
->thenIfErrored(function () {
echo "Bad";
})
->executeInSubprocess();
$this->assertContains("Good", $out);
}
public function testDoesntAffectHandledErrors()
{
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
list($out, $code) = $chain
->then(function () {
set_error_handler(
function () {
/* NOP */
}
);
user_error('Error', E_USER_ERROR);
})
->then(function () {
echo "Good";
})
->thenIfErrored(function () {
echo "Bad";
})
->executeInSubprocess();
$this->assertContains("Good", $out);
}
public function testMemoryConversion()
{
$chain = new ErrorControlChainTest\ErrorControlChainTest_Chain();
$this->assertEquals(200, $chain->translateMemstring('200'));
$this->assertEquals(300, $chain->translateMemstring('300'));
$this->assertEquals(2 * 1024, $chain->translateMemstring('2k'));
$this->assertEquals(3 * 1024, $chain->translateMemstring('3K'));
$this->assertEquals(2 * 1024 * 1024, $chain->translateMemstring('2m'));
$this->assertEquals(3 * 1024 * 1024, $chain->translateMemstring('3M'));
$this->assertEquals(2 * 1024 * 1024 * 1024, $chain->translateMemstring('2g'));
$this->assertEquals(3 * 1024 * 1024 * 1024, $chain->translateMemstring('3G'));
$this->assertEquals(200, $chain->translateMemstring('200foo'));
$this->assertEquals(300, $chain->translateMemstring('300foo'));
}
}

View File

@ -1,96 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Startup\ErrorControlChainTest;
use ReflectionFunction;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Startup\ErrorControlChain;
/**
* An extension of ErrorControlChain that runs the chain in a subprocess.
*
* We need this because ErrorControlChain only suppresses uncaught fatal errors, and
* that would kill PHPUnit execution
*/
class ErrorControlChainTest_Chain extends ErrorControlChain
{
protected $displayErrors = 'STDERR';
/**
* Modify method visibility to public for testing
*
* @return string
*/
public function getDisplayErrors()
{
// Protect manipulation of underlying php_ini values
return $this->displayErrors;
}
/**
* Modify method visibility to public for testing
*
* @param mixed $errors
*/
public function setDisplayErrors($errors)
{
// Protect manipulation of underlying php_ini values
$this->displayErrors = $errors;
}
// Change function visibility to be testable directly
public function translateMemstring($memstring)
{
return parent::translateMemstring($memstring);
}
function executeInSubprocess($includeStderr = false)
{
// Get the path to the ErrorControlChain class
$erroControlClass = 'SilverStripe\\Core\\Startup\\ErrorControlChain';
$classpath = ClassLoader::inst()->getItemPath($erroControlClass);
$suppression = $this->suppression ? 'true' : 'false';
// Start building a PHP file that will execute the chain
$src = '<' . "?php
require_once '$classpath';
\$chain = new $erroControlClass();
\$chain->setSuppression($suppression);
\$chain
";
// For each step, use reflection to pull out the call, stick in the the PHP source we're building
foreach ($this->steps as $step) {
$func = new ReflectionFunction($step['callback']);
$source = file($func->getFileName());
$start_line = $func->getStartLine() - 1;
$end_line = $func->getEndLine();
$length = $end_line - $start_line;
$src .= implode("", array_slice($source, $start_line, $length)) . "\n";
}
// Finally add a line to execute the chain
$src .= "->execute();";
// Now stick it in a temporary file & run it
$codepath = TEMP_PATH . DIRECTORY_SEPARATOR . 'ErrorControlChainTest_' . sha1($src) . '.php';
if ($includeStderr) {
$null = '&1';
} else {
$null = is_writeable('/dev/null') ? '/dev/null' : 'NUL';
}
file_put_contents($codepath, $src);
exec("php $codepath 2>$null", $stdout, $errcode);
unlink($codepath);
return array(implode("\n", $stdout), $errcode);
}
}

View File

@ -1,170 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Startup;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_Token;
use SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_ValidToken;
use SilverStripe\Dev\SapphireTest;
class ParameterConfirmationTokenTest extends SapphireTest
{
/**
* @var HTTPRequest
*/
protected $request = null;
protected function setUp()
{
parent::setUp();
$_GET = [];
$_GET['parameterconfirmationtokentest_notoken'] = 'value';
$_GET['parameterconfirmationtokentest_empty'] = '';
$_GET['parameterconfirmationtokentest_withtoken'] = '1';
$_GET['parameterconfirmationtokentest_withtokentoken'] = 'dummy';
$_GET['parameterconfirmationtokentest_nulltoken'] = '1';
$_GET['parameterconfirmationtokentest_nulltokentoken'] = null;
$_GET['parameterconfirmationtokentest_emptytoken'] = '1';
$_GET['parameterconfirmationtokentest_emptytokentoken'] = '';
$_GET['BackURL'] = 'page?parameterconfirmationtokentest_backtoken=1';
$this->request = new HTTPRequest('GET', 'anotherpage', $_GET);
$this->request->setSession(new Session([]));
}
public function testParameterDetectsParameters()
{
$withoutToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_notoken', $this->request);
$emptyParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_empty', $this->request);
$withToken = new ParameterConfirmationTokenTest_ValidToken('parameterconfirmationtokentest_withtoken', $this->request);
$withoutParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_noparam', $this->request);
$nullToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_nulltoken', $this->request);
$emptyToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_emptytoken', $this->request);
$backToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_backtoken', $this->request);
// Check parameter
$this->assertTrue($withoutToken->parameterProvided());
$this->assertTrue($emptyParameter->parameterProvided()); // even if empty, it's still provided
$this->assertTrue($withToken->parameterProvided());
$this->assertFalse($withoutParameter->parameterProvided());
$this->assertTrue($nullToken->parameterProvided());
$this->assertTrue($emptyToken->parameterProvided());
$this->assertFalse($backToken->parameterProvided());
// Check backurl
$this->assertFalse($withoutToken->existsInReferer());
$this->assertFalse($emptyParameter->existsInReferer()); // even if empty, it's still provided
$this->assertFalse($withToken->existsInReferer());
$this->assertFalse($withoutParameter->existsInReferer());
$this->assertFalse($nullToken->existsInReferer());
$this->assertFalse($emptyToken->existsInReferer());
$this->assertTrue($backToken->existsInReferer());
// Check token
$this->assertFalse($withoutToken->tokenProvided());
$this->assertFalse($emptyParameter->tokenProvided());
$this->assertTrue($withToken->tokenProvided()); // Actually forced to true for this test
$this->assertFalse($withoutParameter->tokenProvided());
$this->assertFalse($nullToken->tokenProvided());
$this->assertFalse($emptyToken->tokenProvided());
$this->assertFalse($backToken->tokenProvided());
// Check if reload is required
$this->assertTrue($withoutToken->reloadRequired());
$this->assertTrue($emptyParameter->reloadRequired());
$this->assertFalse($withToken->reloadRequired());
$this->assertFalse($withoutParameter->reloadRequired());
$this->assertTrue($nullToken->reloadRequired());
$this->assertTrue($emptyToken->reloadRequired());
$this->assertFalse($backToken->reloadRequired());
// Check if a reload is required in case of error
$this->assertTrue($withoutToken->reloadRequiredIfError());
$this->assertTrue($emptyParameter->reloadRequiredIfError());
$this->assertFalse($withToken->reloadRequiredIfError());
$this->assertFalse($withoutParameter->reloadRequiredIfError());
$this->assertTrue($nullToken->reloadRequiredIfError());
$this->assertTrue($emptyToken->reloadRequiredIfError());
$this->assertTrue($backToken->reloadRequiredIfError());
// Check redirect url
$home = (BASE_URL ?: '/') . '?';
$current = Controller::join_links(BASE_URL, '/', 'anotherpage') . '?';
$this->assertStringStartsWith($current, $withoutToken->redirectURL());
$this->assertStringStartsWith($current, $emptyParameter->redirectURL());
$this->assertStringStartsWith($current, $nullToken->redirectURL());
$this->assertStringStartsWith($current, $emptyToken->redirectURL());
$this->assertStringStartsWith($home, $backToken->redirectURL());
// Check suppression
$this->assertEquals('value', $this->request->getVar('parameterconfirmationtokentest_notoken'));
$withoutToken->suppress();
$this->assertNull($this->request->getVar('parameterconfirmationtokentest_notoken'));
}
public function testPrepareTokens()
{
// Test priority ordering
$token = ParameterConfirmationToken::prepare_tokens(
[
'parameterconfirmationtokentest_notoken',
'parameterconfirmationtokentest_empty',
'parameterconfirmationtokentest_noparam'
],
$this->request
);
// Test no invalid tokens
$this->assertEquals('parameterconfirmationtokentest_empty', $token->getName());
$token = ParameterConfirmationToken::prepare_tokens(
[ 'parameterconfirmationtokentest_noparam' ],
$this->request
);
$this->assertEmpty($token);
// Test backurl token
$token = ParameterConfirmationToken::prepare_tokens(
[ 'parameterconfirmationtokentest_backtoken' ],
$this->request
);
$this->assertEquals('parameterconfirmationtokentest_backtoken', $token->getName());
// Test prepare_tokens() unsets $_GET vars
$this->assertArrayNotHasKey('parameterconfirmationtokentest_notoken', $_GET);
$this->assertArrayNotHasKey('parameterconfirmationtokentest_empty', $_GET);
$this->assertArrayNotHasKey('parameterconfirmationtokentest_noparam', $_GET);
}
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)
{
$this->request->setUrl($url);
$token = new ParameterConfirmationTokenTest_Token(
'parameterconfirmationtokentest_parameter',
$this->request
);
$expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/';
$this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url");
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\Dev\TestOnly;
/**
* Dummy parameter token
*/
class ParameterConfirmationTokenTest_Token extends ParameterConfirmationToken implements TestOnly
{
public function currentURL()
{
return parent::currentURL();
}
public function redirectURL()
{
return parent::redirectURL();
}
}

View File

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

View File

@ -1,148 +0,0 @@
<?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

@ -1,27 +0,0 @@
<?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

@ -1,15 +0,0 @@
<?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,157 @@
<?php
namespace SilverStripe\Security\Tests\Confirmation;
use SilverStripe\Control\Session;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Confirmation\Storage;
use SilverStripe\Security\Confirmation\Item;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
class StorageTest extends SapphireTest
{
use HttpRequestMockBuilder;
private function getNamespace($id)
{
return str_replace('\\', '.', Storage::class).'.'.$id;
}
public function testNewStorage()
{
$session = $this->createMock(Session::class);
$sessionCleaned = false;
$session->method('clear')->will($this->returnCallback(static function ($namespace) use (&$sessionCleaned) {
$sessionCleaned = $namespace;
}));
$storage = new Storage($session, 'test');
$this->assertEquals(
$this->getNamespace('test'),
$sessionCleaned,
'Session data should have been cleaned from the obsolete data'
);
$sessionCleaned = false;
$storage = new Storage($session, 'test', false);
$this->assertFalse($sessionCleaned, 'Session data should have been preserved');
}
public function testCleanup()
{
$session = $this->createMock(Session::class);
$sessionCleaned = false;
$session->method('clear')->will($this->returnCallback(static function ($namespace) use (&$sessionCleaned) {
$sessionCleaned = $namespace;
}));
$storage = new Storage($session, 'test', false);
$this->assertFalse($sessionCleaned, 'Session data should have been preserved');
$storage->cleanup();
$this->assertEquals(
$this->getNamespace('test'),
$sessionCleaned,
'Session data should have been cleaned up'
);
}
public function testSuccessRequest()
{
$session = new Session([]);
$storage = new Storage($session, 'test');
$request = $this->buildRequestMock('dev/build', ['flush' => 'all']);
$storage->setSuccessRequest($request);
// ensure the data is persisted within the session
$storage = new Storage($session, 'test', false);
$this->assertEquals('dev/build?flush=all', $storage->getSuccessUrl());
$this->assertEquals('GET', $storage->getHttpMethod());
}
public function testPutItem()
{
$session = new Session([]);
$storage = new Storage($session, 'test');
$item1 = new Item('item1_token', 'item1_name', 'item1_desc');
$item2 = new Item('item2_token', 'item2_name', 'item2_desc');
$storage->putItem($item1);
$storage->putItem($item2);
// ensure the data is persisted within the session
$storage = new Storage($session, 'test', false);
$items = $storage->getItems();
$hashedItems = $storage->getHashedItems();
$this->assertCount(2, $items);
$this->assertCount(2, $hashedItems);
$item1Hash = $storage->getTokenHash($item1);
$this->assertArrayHasKey($item1Hash, $items);
$item = $items[$item1Hash];
$this->assertEquals('item1_token', $item->getToken());
$this->assertEquals('item1_name', $item->getName());
$this->assertEquals('item1_desc', $item->getDescription());
$this->assertFalse($item->isConfirmed());
$item2Hash = $storage->getTokenHash($item2);
$this->assertArrayHasKey($item2Hash, $items);
$item = $items[$item2Hash];
$this->assertEquals('item2_token', $item->getToken());
$this->assertEquals('item2_name', $item->getName());
$this->assertEquals('item2_desc', $item->getDescription());
$this->assertFalse($item->isConfirmed());
}
public function testConfirmation()
{
$session = new Session([]);
$storage = new Storage($session, 'test');
$item1 = new Item('item1_token', 'item1_name', 'item1_desc');
$item2 = new Item('item2_token', 'item2_name', 'item2_desc');
$storage->putItem($item1);
$storage->putItem($item2);
// ensure the data is persisted within the session
$storage = new Storage($session, 'test', false);
foreach ($storage->getItems() as $item) {
$this->assertFalse($item->isConfirmed());
}
$this->assertFalse($storage->check([$item1, $item2]));
// check we cannot confirm items with incorrect data
$storage->confirm([]);
foreach ($storage->getItems() as $item) {
$this->assertFalse($item->isConfirmed());
}
$this->assertFalse($storage->check([$item1, $item2]));
// check we cannot confirm items with unsalted tokens
$storage->confirm(['item1_token' => '1', 'item2_token' => '1']);
foreach ($storage->getItems() as $item) {
$this->assertFalse($item->isConfirmed());
}
$this->assertFalse($storage->check([$item1, $item2]));
// check we can confirm data with properly salted tokens
$storage->confirm($storage->getHashedItems());
foreach ($storage->getItems() as $item) {
$this->assertTrue($item->isConfirmed());
}
$this->assertTrue($storage->check([$item1, $item2]));
}
}

View File

@ -3,6 +3,7 @@
namespace SilverStripe\View\Tests; namespace SilverStripe\View\Tests;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\TempFolder; use SilverStripe\Core\TempFolder;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
@ -153,6 +154,7 @@ class SSViewerCacheBlockTest extends SapphireTest
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 3)), '1'); $this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 3)), '1');
// Test with flush // Test with flush
Injector::inst()->get(Kernel::class)->boot(true);
Director::test('/?flush=1'); Director::test('/?flush=1');
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 2)), '2'); $this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 2)), '2');
} }