mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '4.3' into 4.4
This commit is contained in:
commit
c747b1f8d3
35
_config/confirmation-middleware.yml
Normal file
35
_config/confirmation-middleware.yml
Normal 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
|
@ -11,3 +11,5 @@ SilverStripe\Dev\DevelopmentAdmin:
|
||||
controller: SilverStripe\Dev\TaskRunner
|
||||
links:
|
||||
tasks: 'See a list of build tasks to run'
|
||||
confirm:
|
||||
controller: SilverStripe\Dev\DevConfirmationController
|
||||
|
@ -32,6 +32,7 @@ SilverStripe\Core\Injector\Injector:
|
||||
RequestHandler: '%$SilverStripe\Security\Security'
|
||||
Middlewares:
|
||||
- '%$SecurityRateLimitMiddleware'
|
||||
|
||||
---
|
||||
Name: errorrequestprocessors
|
||||
After:
|
||||
@ -40,6 +41,8 @@ After:
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
# Note: If Director config changes, take note it will affect this config too
|
||||
SilverStripe\Core\Startup\ErrorDirector: '%$SilverStripe\Control\Director'
|
||||
|
||||
|
||||
---
|
||||
Name: canonicalurls
|
||||
---
|
||||
@ -48,3 +51,94 @@ SilverStripe\Core\Injector\Injector:
|
||||
properties:
|
||||
ForceSSL: 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"])'
|
||||
|
@ -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_CA` | Absolute path to SSL Certificate Authority bundle file |
|
||||
| `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. |
|
||||
|
@ -118,4 +118,5 @@ SilverStripe\Control\Director:
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [Built-in Middleware](./06_Builtin_Middlewares.md)
|
||||
* [HTTPMiddleware](api:SilverStripe\Control\Middleware\HTTPMiddleware)
|
||||
|
@ -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) |
|
@ -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
|
||||
the CMS logic. Add a new section into the `<ul class="cms-menu__list">`
|
||||
|
||||
|
||||
```ss
|
||||
...
|
||||
<ul class="cms-menu-list">
|
||||
@ -243,7 +242,7 @@ how-to.
|
||||
|
||||
## React-rendered UI
|
||||
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.
|
||||
|
||||
### Implementing handlers
|
||||
|
@ -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
|
||||
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
|
||||
|
||||
To use this API, you need to make your class implement [Flushable](api:SilverStripe\Core\Flushable), and define a `flush()` static function,
|
||||
|
@ -74,6 +74,11 @@ sake dev/
|
||||
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..
|
||||
|
||||
```bash
|
||||
|
@ -1,9 +1,10 @@
|
||||
# 4.4.0 (Unreleased)
|
||||
# 4.4.0
|
||||
|
||||
## Overview {#overview}
|
||||
|
||||
- [Optional migration to hash-less public asset URLs](#hash-less)
|
||||
- [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)
|
||||
- [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+.
|
||||
@ -13,8 +14,29 @@
|
||||
- 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
|
||||
|
||||
|
||||
### 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}
|
||||
|
||||
### If you are migrating files from SilverStripe 3
|
||||
You’ll want to use the 4.4.1 release if you’re 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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
- `PDOQuery::__construct()` now has a 2nd argument. If you have subclassed PDOQuery and overridden __construct()
|
||||
you may see an E_STRICT error
|
||||
- The name of the directory where vendor module resources are exposed can now be configured by adding a `extra.resources-dir` key to your composer file. The new default in `silverstripe/installer` has been changed to `_resources` rather than `resources`. This allows you to use `resources` as a URL segment or a route.
|
||||
- `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->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->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 doesn’t 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 PDO’s 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)
|
@ -375,6 +375,33 @@ class Director implements TemplateGlobalProvider
|
||||
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
|
||||
* object to return, then this will return the current controller.
|
||||
@ -1018,7 +1045,7 @@ class Director implements TemplateGlobalProvider
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* environment types, see {@link Director::set_environment_type()}.
|
||||
|
@ -4,7 +4,14 @@ namespace SilverStripe\Control;
|
||||
|
||||
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
|
||||
use SilverStripe\Core\Application;
|
||||
use SilverStripe\Core\Environment;
|
||||
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
|
||||
@ -18,11 +25,73 @@ class HTTPApplication implements Application
|
||||
*/
|
||||
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)
|
||||
{
|
||||
$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
|
||||
*
|
||||
@ -41,10 +110,10 @@ class HTTPApplication implements Application
|
||||
*/
|
||||
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
|
||||
return $this->execute($request, function (HTTPRequest $request) {
|
||||
return $this->execute($request, static function (HTTPRequest $request) {
|
||||
return Director::singleton()->handleRequest($request);
|
||||
}, $flush);
|
||||
}
|
||||
@ -55,6 +124,7 @@ class HTTPApplication implements Application
|
||||
* @param HTTPRequest $request
|
||||
* @param callable $callback
|
||||
* @param bool $flush
|
||||
*
|
||||
* @return HTTPResponse
|
||||
*/
|
||||
public function execute(HTTPRequest $request, callable $callback, $flush = false)
|
||||
|
@ -13,8 +13,10 @@ use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
|
||||
/**
|
||||
* Allows events to be registered and passed through middleware.
|
||||
* Useful for event registered prior to the beginning of a middleware chain.
|
||||
* Implements the following URL normalisation rules
|
||||
* - redirect basic auth requests to HTTPS
|
||||
* - force WWW, redirect to the subdomain "www."
|
||||
* - force SSL, redirect to https
|
||||
*/
|
||||
class CanonicalURLMiddleware implements HTTPMiddleware
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ use SilverStripe\Core\Injector\Injectable;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
|
295
src/Control/Middleware/ConfirmationMiddleware.php
Normal file
295
src/Control/Middleware/ConfirmationMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
25
src/Control/Middleware/ConfirmationMiddleware/AjaxBypass.php
Normal file
25
src/Control/Middleware/ConfirmationMiddleware/AjaxBypass.php
Normal 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();
|
||||
}
|
||||
}
|
22
src/Control/Middleware/ConfirmationMiddleware/Bypass.php
Normal file
22
src/Control/Middleware/ConfirmationMiddleware/Bypass.php
Normal 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);
|
||||
}
|
25
src/Control/Middleware/ConfirmationMiddleware/CliBypass.php
Normal file
25
src/Control/Middleware/ConfirmationMiddleware/CliBypass.php
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
112
src/Control/Middleware/ConfirmationMiddleware/GetParameter.php
Normal file
112
src/Control/Middleware/ConfirmationMiddleware/GetParameter.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
53
src/Control/Middleware/ConfirmationMiddleware/PathAware.php
Normal file
53
src/Control/Middleware/ConfirmationMiddleware/PathAware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
22
src/Control/Middleware/ConfirmationMiddleware/Rule.php
Normal file
22
src/Control/Middleware/ConfirmationMiddleware/Rule.php
Normal 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);
|
||||
}
|
197
src/Control/Middleware/ConfirmationMiddleware/Url.php
Normal file
197
src/Control/Middleware/ConfirmationMiddleware/Url.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ use SilverStripe\Control\HTTPResponse;
|
||||
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
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Control\Middleware;
|
||||
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Flushable;
|
||||
@ -11,12 +12,9 @@ use SilverStripe\Core\Flushable;
|
||||
*/
|
||||
class FlushMiddleware implements HTTPMiddleware
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function process(HTTPRequest $request, callable $delegate)
|
||||
{
|
||||
if (array_key_exists('flush', $request->getVars())) {
|
||||
if (Director::isManifestFlushed()) {
|
||||
// Disable cache when flushing
|
||||
HTTPCacheControlMiddleware::singleton()->disableCache(true);
|
||||
|
||||
|
159
src/Control/Middleware/PermissionAwareConfirmationMiddleware.php
Normal file
159
src/Control/Middleware/PermissionAwareConfirmationMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
78
src/Control/Middleware/URLSpecialsMiddleware.php
Normal file
78
src/Control/Middleware/URLSpecialsMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -77,6 +77,21 @@ class CoreKernel implements Kernel
|
||||
|
||||
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
|
||||
*
|
||||
@ -126,6 +141,13 @@ class CoreKernel implements Kernel
|
||||
$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()
|
||||
{
|
||||
// Check set
|
||||
@ -151,41 +173,23 @@ class CoreKernel implements Kernel
|
||||
* Check or update any temporary environment specified in the session.
|
||||
*
|
||||
* @return null|string
|
||||
*
|
||||
* @deprecated 5.0 Use Director::get_session_environment_type() instead
|
||||
*/
|
||||
protected function sessionEnvironment()
|
||||
{
|
||||
// Check isDev in querystring
|
||||
if (isset($_GET['isDev'])) {
|
||||
if (isset($_SESSION)) {
|
||||
unset($_SESSION['isTest']); // In case we are changing from test mode
|
||||
$_SESSION['isDev'] = $_GET['isDev'];
|
||||
}
|
||||
return self::DEV;
|
||||
if (!$this->booted) {
|
||||
// session is not initialyzed yet, neither is manifest
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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 Director::get_session_environment_type();
|
||||
}
|
||||
|
||||
public function boot($flush = false)
|
||||
{
|
||||
$this->flush = $flush;
|
||||
|
||||
$this->bootPHP();
|
||||
$this->bootManifests($flush);
|
||||
$this->bootErrorHandling();
|
||||
@ -193,6 +197,8 @@ class CoreKernel implements Kernel
|
||||
$this->bootConfigs();
|
||||
$this->bootDatabaseGlobals();
|
||||
$this->validateDatabase();
|
||||
|
||||
$this->booted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -655,4 +661,14 @@ class CoreKernel implements Kernel
|
||||
$this->themeResourceLoader = $themeResourceLoader;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -222,4 +222,14 @@ class Environment
|
||||
{
|
||||
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']);
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +167,14 @@ class ClassManifest
|
||||
*/
|
||||
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
|
||||
* from the cache or re-scanning for classes.
|
||||
@ -181,6 +189,72 @@ class ClassManifest
|
||||
$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
|
||||
*
|
||||
@ -189,13 +263,7 @@ class ClassManifest
|
||||
*/
|
||||
public function init($includeTests = false, $forceRegen = false)
|
||||
{
|
||||
// build cache from factory
|
||||
if ($this->cacheFactory) {
|
||||
$this->cache = $this->cacheFactory->create(
|
||||
CacheInterface::class . '.classmanifest',
|
||||
['namespace' => 'classmanifest' . ($includeTests ? '_tests' : '')]
|
||||
);
|
||||
}
|
||||
$this->cache = $this->buildCache($includeTests);
|
||||
|
||||
// Check if cache is safe to use
|
||||
if (!$forceRegen
|
||||
@ -458,7 +526,11 @@ class ClassManifest
|
||||
if ($this->cache) {
|
||||
$data = $this->getState();
|
||||
$this->cache->set($this->cacheKey, $data);
|
||||
$this->cache->set('generated_at', time());
|
||||
$this->cache->delete('regenerate');
|
||||
}
|
||||
|
||||
$this->cacheRegenerated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,6 +14,8 @@ use SilverStripe\Security\RandomGenerator;
|
||||
* string parameters
|
||||
*
|
||||
* @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
|
||||
{
|
||||
|
31
src/Core/Startup/CallbackFlushDiscoverer.php
Normal file
31
src/Core/Startup/CallbackFlushDiscoverer.php
Normal 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);
|
||||
}
|
||||
}
|
22
src/Core/Startup/CompositeFlushDiscoverer.php
Normal file
22
src/Core/Startup/CompositeFlushDiscoverer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,8 @@ use SilverStripe\Core\Convert;
|
||||
* 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
|
||||
*
|
||||
* @deprecated 5.0 To be removed in SilverStripe 5.0
|
||||
*/
|
||||
class ConfirmationTokenChain
|
||||
{
|
||||
|
120
src/Core/Startup/DeployFlushDiscoverer.php
Normal file
120
src/Core/Startup/DeployFlushDiscoverer.php
Normal 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;
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@ use Exception;
|
||||
* $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute();
|
||||
*
|
||||
* @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
|
||||
{
|
||||
|
@ -8,12 +8,15 @@ use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Control\HTTPResponse_Exception;
|
||||
use SilverStripe\Control\Middleware\HTTPMiddleware;
|
||||
use SilverStripe\Core\Application;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
/**
|
||||
* Decorates application bootstrapping with errorcontrolchain
|
||||
*
|
||||
* @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
|
||||
{
|
||||
@ -22,14 +25,24 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
|
||||
*/
|
||||
protected $application = null;
|
||||
|
||||
/**
|
||||
* Whether to keep working (legacy mode)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $legacy;
|
||||
|
||||
/**
|
||||
* Build error control chain for an 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->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)
|
||||
{
|
||||
if (!$this->legacy) {
|
||||
return call_user_func($next, $request);
|
||||
}
|
||||
|
||||
$result = null;
|
||||
|
||||
// Prepare tokens and execute chain
|
||||
|
@ -14,6 +14,8 @@ use SilverStripe\Security\Security;
|
||||
* Specialised Director class used by ErrorControlChain to handle error and redirect conditions
|
||||
*
|
||||
* @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
|
||||
{
|
||||
|
20
src/Core/Startup/FlushDiscoverer.php
Normal file
20
src/Core/Startup/FlushDiscoverer.php
Normal 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();
|
||||
}
|
@ -15,6 +15,8 @@ use SilverStripe\Security\RandomGenerator;
|
||||
* redirected URL
|
||||
*
|
||||
* @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
|
||||
{
|
||||
|
88
src/Core/Startup/RequestFlushDiscoverer.php
Normal file
88
src/Core/Startup/RequestFlushDiscoverer.php
Normal 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();
|
||||
}
|
||||
}
|
66
src/Core/Startup/ScheduledFlushDiscoverer.php
Normal file
66
src/Core/Startup/ScheduledFlushDiscoverer.php
Normal 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;
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ use SilverStripe\Control\HTTPRequest;
|
||||
* 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
|
||||
*
|
||||
* @deprecated 5.0 To be removed in SilverStripe 5.0
|
||||
*/
|
||||
class URLConfirmationToken extends AbstractConfirmationToken
|
||||
{
|
||||
|
33
src/Dev/DevConfirmationController.php
Normal file
33
src/Dev/DevConfirmationController.php
Normal 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;
|
||||
}
|
||||
}
|
@ -52,10 +52,22 @@ class DevelopmentAdmin extends Controller
|
||||
*/
|
||||
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()
|
||||
{
|
||||
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)
|
||||
$requestedDevBuild = (stripos($this->getRequest()->getURL(), 'dev/build') === 0)
|
||||
&& (stripos($this->getRequest()->getURL(), 'dev/build/defaults') === false);
|
||||
|
@ -13,10 +13,10 @@ use SilverStripe\Core\CoreKernel;
|
||||
use SilverStripe\Core\EnvironmentLoader;
|
||||
use SilverStripe\Core\Kernel;
|
||||
use SilverStripe\Core\Path;
|
||||
use SilverStripe\Core\Startup\ParameterConfirmationToken;
|
||||
use SilverStripe\ORM\DatabaseAdmin;
|
||||
use SilverStripe\Security\DefaultAdminService;
|
||||
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.
|
||||
@ -24,6 +24,7 @@ use SilverStripe\Security\Security;
|
||||
class Installer
|
||||
{
|
||||
use InstallEnvironmentAware;
|
||||
use SessionEnvTypeSwitcher;
|
||||
|
||||
/**
|
||||
* Errors during install
|
||||
@ -203,12 +204,19 @@ PHP
|
||||
|
||||
// Check result of install
|
||||
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()) {
|
||||
$this->statusMessage("Checking that friendly URLs work...");
|
||||
$this->checkRewrite();
|
||||
} else {
|
||||
$token = new ParameterConfirmationToken('flush', $request);
|
||||
$params = http_build_query($token->params());
|
||||
$params = http_build_query($request->getVars() + ['flush' => '']);
|
||||
|
||||
$destinationURL = 'index.php/' .
|
||||
($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
|
||||
@ -247,7 +255,6 @@ HTML;
|
||||
use SilverStripe\Control\HTTPApplication;
|
||||
use SilverStripe\Control\HTTPRequestBuilder;
|
||||
use SilverStripe\Core\CoreKernel;
|
||||
use SilverStripe\Core\Startup\ErrorControlChainMiddleware;
|
||||
|
||||
// Find autoload.php
|
||||
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||
@ -265,7 +272,6 @@ $request = HTTPRequestBuilder::createFromEnvironment();
|
||||
// Default application
|
||||
$kernel = new CoreKernel(BASE_PATH);
|
||||
$app = new HTTPApplication($kernel);
|
||||
$app->addMiddleware(new ErrorControlChainMiddleware($app));
|
||||
$response = $app->handle($request);
|
||||
$response->output();
|
||||
PHP;
|
||||
@ -598,8 +604,7 @@ TEXT;
|
||||
|
||||
public function checkRewrite()
|
||||
{
|
||||
$token = new ParameterConfirmationToken('flush', new HTTPRequest('GET', '/'));
|
||||
$params = http_build_query($token->params());
|
||||
$params = http_build_query(['flush' => '']);
|
||||
|
||||
$destinationURL = rtrim(BASE_URL, '/') . '/' . (
|
||||
$this->checkModuleExists('cms')
|
||||
|
171
src/Security/Confirmation/Form.php
Normal file
171
src/Security/Confirmation/Form.php
Normal 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'))
|
||||
);
|
||||
}
|
||||
}
|
87
src/Security/Confirmation/Handler.php
Normal file
87
src/Security/Confirmation/Handler.php
Normal 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__);
|
||||
}
|
||||
}
|
103
src/Security/Confirmation/Item.php
Normal file
103
src/Security/Confirmation/Item.php
Normal 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;
|
||||
}
|
||||
}
|
444
src/Security/Confirmation/Storage.php
Normal file
444
src/Security/Confirmation/Storage.php
Normal 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 : ''
|
||||
);
|
||||
}
|
||||
}
|
@ -37,13 +37,13 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
{
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'basicauthlogin',
|
||||
'changepassword',
|
||||
'index',
|
||||
'login',
|
||||
'logout',
|
||||
'basicauthlogin',
|
||||
'lostpassword',
|
||||
'passwordsent',
|
||||
'changepassword',
|
||||
'ping',
|
||||
);
|
||||
|
||||
@ -661,7 +661,6 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
->clear("Security.Message");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the "login" page
|
||||
*
|
||||
|
@ -14,6 +14,7 @@ use SilverStripe\Control\RequestProcessor;
|
||||
use SilverStripe\Control\Tests\DirectorTest\TestController;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Environment;
|
||||
use SilverStripe\Core\Kernel;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
@ -26,11 +27,15 @@ class DirectorTest extends SapphireTest
|
||||
TestController::class,
|
||||
];
|
||||
|
||||
private $originalEnvType;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
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
|
||||
CanonicalURLMiddleware::singleton()
|
||||
->setForceSSLDomain(null)
|
||||
@ -39,6 +44,12 @@ class DirectorTest extends SapphireTest
|
||||
$this->expectedRedirect = null;
|
||||
}
|
||||
|
||||
protected function tearDown(...$args)
|
||||
{
|
||||
Environment::setEnv('SS_ENVIRONMENT_TYPE', $this->originalEnvType);
|
||||
parent::tearDown(...$args);
|
||||
}
|
||||
|
||||
protected function 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()
|
||||
{
|
||||
@ -422,30 +433,33 @@ class DirectorTest extends SapphireTest
|
||||
/** @var Kernel $kernel */
|
||||
$kernel = Injector::inst()->get(Kernel::class);
|
||||
$kernel->setEnvironment(null);
|
||||
Environment::setEnv('SS_ENVIRONMENT_TYPE', Kernel::LIVE);
|
||||
|
||||
$this->assertTrue(Director::isLive());
|
||||
|
||||
// Test isDev=1
|
||||
$_GET['isDev'] = '1';
|
||||
$this->assertTrue(Director::isDev());
|
||||
$this->assertFalse(Director::isDev());
|
||||
$this->assertFalse(Director::isTest());
|
||||
$this->assertFalse(Director::isLive());
|
||||
$this->assertTrue(Director::isLive());
|
||||
|
||||
// Test persistence
|
||||
unset($_GET['isDev']);
|
||||
$this->assertTrue(Director::isDev());
|
||||
$this->assertFalse(Director::isDev());
|
||||
$this->assertFalse(Director::isTest());
|
||||
$this->assertFalse(Director::isLive());
|
||||
$this->assertTrue(Director::isLive());
|
||||
|
||||
// Test change to isTest
|
||||
$_GET['isTest'] = '1';
|
||||
$this->assertFalse(Director::isDev());
|
||||
$this->assertTrue(Director::isTest());
|
||||
$this->assertFalse(Director::isLive());
|
||||
$this->assertFalse(Director::isTest());
|
||||
$this->assertTrue(Director::isLive());
|
||||
|
||||
// Test persistence
|
||||
unset($_GET['isTest']);
|
||||
$this->assertFalse(Director::isDev());
|
||||
$this->assertTrue(Director::isTest());
|
||||
$this->assertFalse(Director::isLive());
|
||||
$this->assertFalse(Director::isTest());
|
||||
$this->assertTrue(Director::isLive());
|
||||
}
|
||||
|
||||
public function testResetGlobalsAfterTestRequest()
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Control\Tests;
|
||||
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Kernel;
|
||||
use SilverStripe\Control\Tests\FlushMiddlewareTest\TestFlushable;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
|
||||
@ -13,7 +15,12 @@ class FlushMiddlewareTest extends FunctionalTest
|
||||
public function testImplementorsAreCalled()
|
||||
{
|
||||
TestFlushable::$flushed = false;
|
||||
$this->get('?flush=1');
|
||||
|
||||
Injector::inst()->get(Kernel::class)->boot(true);
|
||||
$this->get('/');
|
||||
$this->assertTrue(TestFlushable::$flushed);
|
||||
|
||||
// reset the kernel Flush flag
|
||||
Injector::inst()->get(Kernel::class)->boot();
|
||||
}
|
||||
}
|
||||
|
57
tests/php/Control/HttpRequestMockBuilder.php
Normal file
57
tests/php/Control/HttpRequestMockBuilder.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
107
tests/php/Control/Middleware/ConfirmationMiddleware/UrlTest.php
Normal file
107
tests/php/Control/Middleware/ConfirmationMiddleware/UrlTest.php
Normal 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));
|
||||
}
|
||||
}
|
82
tests/php/Control/Middleware/ConfirmationMiddlewareTest.php
Normal file
82
tests/php/Control/Middleware/ConfirmationMiddlewareTest.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -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']);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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'));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
157
tests/php/Security/Confirmation/StorageTest.php
Normal file
157
tests/php/Security/Confirmation/StorageTest.php
Normal 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]));
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\View\Tests;
|
||||
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Kernel;
|
||||
use SilverStripe\Core\TempFolder;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
@ -153,6 +154,7 @@ class SSViewerCacheBlockTest extends SapphireTest
|
||||
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 3)), '1');
|
||||
|
||||
// Test with flush
|
||||
Injector::inst()->get(Kernel::class)->boot(true);
|
||||
Director::test('/?flush=1');
|
||||
$this->assertEquals($this->_runtemplate('<% cached %>$Foo<% end_cached %>', array('Foo' => 2)), '2');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user