API Stronger Injector service unregistration

BUG Fix up test regressions
FIX director references to request object
API Move all middlewares to common namespace
API Implement RequestHandlerMiddlewareAdapter
ENHANCEMENT Improve IP address parsing
Fix up PHPDoc / psr2 linting
BUG Fix property parsing in TrustedProxyMiddleware
BUG Fix Director::is_https()
This commit is contained in:
Damian Mooyman 2017-06-25 15:12:29 +12:00
parent 7aa67f856b
commit d20ab50f9d
29 changed files with 519 additions and 262 deletions

View File

@ -261,7 +261,7 @@ mappings:
Cookie_Backend: SilverStripe\Control\Cookie_Backend Cookie_Backend: SilverStripe\Control\Cookie_Backend
CookieJar: SilverStripe\Control\CookieJar CookieJar: SilverStripe\Control\CookieJar
Director: SilverStripe\Control\Director Director: SilverStripe\Control\Director
FlushRequestFilter: SilverStripe\Control\FlushMiddleware FlushRequestFilter: SilverStripe\Control\Middleware\FlushMiddleware
HTTP: SilverStripe\Control\HTTP HTTP: SilverStripe\Control\HTTP
SS_HTTPRequest: SilverStripe\Control\HTTPRequest SS_HTTPRequest: SilverStripe\Control\HTTPRequest
SS_HTTPResponse: SilverStripe\Control\HTTPResponse SS_HTTPResponse: SilverStripe\Control\HTTPResponse

View File

@ -5,12 +5,11 @@ SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director: SilverStripe\Control\Director:
properties: properties:
Middlewares: Middlewares:
TrustedProxyMiddleware: '%$SilverStripe\Control\TrustedProxyMiddleware' TrustedProxyMiddleware: %$SilverStripe\Control\Middleware\TrustedProxyMiddleware
AllowedHostsMiddleware: '%$SilverStripe\Control\AllowedHostsMiddleware' AllowedHostsMiddleware: %$SilverStripe\Control\Middleware\AllowedHostsMiddleware
SessionMiddleware: '%$SilverStripe\Control\SessionMiddleware' SessionMiddleware: %$SilverStripe\Control\Middleware\SessionMiddleware
RequestProcessor: '%$SilverStripe\Control\RequestProcessor' RequestProcessor: %$SilverStripe\Control\RequestProcessor
FlushMiddleware: '%$SilverStripe\Control\FlushMiddleware' FlushMiddleware: %$SilverStripe\Control\Middleware\FlushMiddleware
SilverStripe\Control\AllowedHostsMiddleware: SilverStripe\Control\AllowedHostsMiddleware:
properties: properties:
AllowedHosts: "`SS_ALLOWED_HOSTS`" AllowedHosts: "`SS_ALLOWED_HOSTS`"

View File

@ -18,12 +18,14 @@ SilverStripe\Core\Injector\Injector:
alc: %$SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler alc: %$SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler
--- ---
Name: coresecurity Name: coresecurity
After:
- requestprocessors
--- ---
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director: SilverStripe\Control\Director:
properties: properties:
Middlewares: Middlewares:
- %$SilverStripe\Security\AuthenticationMiddleware AuthenticationMiddleware: %$SilverStripe\Security\AuthenticationMiddleware
SilverStripe\Security\AuthenticationMiddleware: SilverStripe\Security\AuthenticationMiddleware:
properties: properties:
AuthenticationHandler: %$SilverStripe\Security\AuthenticationHandler AuthenticationHandler: %$SilverStripe\Security\AuthenticationHandler

View File

@ -8,8 +8,9 @@ authentication, logging, caching, request processing, and many other purposes. N
replaces the SilverStripe 3 interface, [api:RequestFilter], which still works but is deprecated. replaces the SilverStripe 3 interface, [api:RequestFilter], which still works but is deprecated.
To create a middleware class, implement `SilverStripe\Control\HTTPMiddleware` and define the To create a middleware class, implement `SilverStripe\Control\HTTPMiddleware` and define the
`process($request, $delegate)` method. You can do anything you like in this method, but to continue `process(HTTPRequest $request, callbale $delegate)` method. You can do anything you like in this
normal execution, you should call `$response = $delegate($request)` at some point in this method. method, but to continue normal execution, you should call `$response = $delegate($request)`
at some point in this method.
In addition, you should return an HTTPResponse object. In normal cases, this should be the In addition, you should return an HTTPResponse object. In normal cases, this should be the
$response object returned by `$delegate`, perhaps with some modification. However, sometimes you $response object returned by `$delegate`, perhaps with some modification. However, sometimes you
@ -20,7 +21,7 @@ will deliberately return a different response, e.g. an error response or a redir
:::php :::php
<?php <?php
use SilverStripe\Control\HTTPMiddleware use SilverStripe\Control\Middleware\HTTPMiddleware
class CustomMiddleware implements HTTPMiddleware { class CustomMiddleware implements HTTPMiddleware {
@ -53,15 +54,24 @@ use of it.
## Global middleware ## Global middleware
By adding the service or class name to the SilverStripe\Control\Director.middlewares array, a By adding the service or class name to the Director::Middlewares property via injector,
middleware will be executed on every request: array, a middleware will be executed on every request:
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml :::yml
SilverStripe\Control\Director: ---
middlewares: Name: myrequestprocessors
- %$CustomMiddleware After:
- requestprocessors
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director:
properties:
Middlewares:
CustomMiddleware: %$CustomMiddleware
Because these are service names, you can configure properties into a custom service if you would Because these are service names, you can configure properties into a custom service if you would
like: like:
@ -69,29 +79,39 @@ like:
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml :::yml
SilverStripe\Control\Director:
middlewares:
- %$ConfiguredMiddleware
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Director:
properties:
Middlewares:
CustomMiddleware: %$ConfiguredMiddleware
ConfiguredMiddleware: ConfiguredMiddleware:
class: $CustomMiddleware class: 'CustomMiddleware'
properties: properties:
Secret: "DIFFERENT-ONE" Secret: "DIFFERENT-ONE"
## Route-specific middleware ## Route-specific middleware
Alternatively, you can apply middlewares to a specific route. These will be processed after the Alternatively, you can apply middlewares to a specific route. These will be processed after the
global middlewares. You do this by specifying the "Middlewares" property of the route rule: global middlewares. You can do this by using the `RequestHandlerMiddlewareAdapter` class
as a replacement for your controller, and register it as a service with a `Middlewares`
property. The controller which does the work should be registered under the
`RequestHandler` property.
**mysite/_config/app.yml** **mysite/_config/app.yml**
:::yml :::yml
SilverStripe\Core\Injector\Injector:
SpecialRouteMiddleware:
class: SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter
properties
RequestHandler: %$MyController
Middlewares:
- %$CustomMiddleware
- %$AnotherMiddleware
SilverStripe\Control\Director: SilverStripe\Control\Director:
rules: rules:
special\section: special\section:
Controller: SpecialSectionController Controller: SpecialRouteMiddleware
Middlewares:
- %$CustomMiddleware
## API Documentation ## API Documentation

View File

@ -84,7 +84,7 @@ class ContentNegotiator
return false; return false;
} }
if (static::config()->enabled) { if (static::config()->get('enabled')) {
return true; return true;
} else { } else {
return (substr($response->getBody(), 0, 5) == '<' . '?xml'); return (substr($response->getBody(), 0, 5) == '<' . '?xml');
@ -106,7 +106,7 @@ class ContentNegotiator
); );
$q = array(); $q = array();
if (headers_sent()) { if (headers_sent()) {
$chosenFormat = static::config()->default_format; $chosenFormat = static::config()->get('default_format');
} elseif (isset($_GET['forceFormat'])) { } elseif (isset($_GET['forceFormat'])) {
$chosenFormat = $_GET['forceFormat']; $chosenFormat = $_GET['forceFormat'];
} else { } else {

View File

@ -3,8 +3,10 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Environment; use SilverStripe\Core\Environment;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel; use SilverStripe\Core\Kernel;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
@ -29,6 +31,7 @@ use SilverStripe\View\TemplateGlobalProvider;
class Director implements TemplateGlobalProvider class Director implements TemplateGlobalProvider
{ {
use Configurable; use Configurable;
use Injectable;
use HTTPMiddlewareAware; use HTTPMiddlewareAware;
/** /**
@ -133,7 +136,7 @@ class Director implements TemplateGlobalProvider
) { ) {
return static::mockRequest( return static::mockRequest(
function (HTTPRequest $request) { function (HTTPRequest $request) {
return Injector::inst()->get(Director::class)->handleRequest($request); return Director::singleton()->handleRequest($request);
}, },
$url, $url,
$postVars, $postVars,
@ -315,6 +318,12 @@ class Director implements TemplateGlobalProvider
}; };
foreach ($rules as $pattern => $controllerOptions) { foreach ($rules as $pattern => $controllerOptions) {
// Match pattern
$arguments = $request->match($pattern, true);
if ($arguments == false) {
continue;
}
// Normalise route rule // Normalise route rule
if (is_string($controllerOptions)) { if (is_string($controllerOptions)) {
if (substr($controllerOptions, 0, 2) == '->') { if (substr($controllerOptions, 0, 2) == '->') {
@ -323,57 +332,39 @@ class Director implements TemplateGlobalProvider
$controllerOptions = array('Controller' => $controllerOptions); $controllerOptions = array('Controller' => $controllerOptions);
} }
} }
$request->setRouteParams($controllerOptions);
// Match pattern // controllerOptions provide some default arguments
$arguments = $request->match($pattern, true); $arguments = array_merge($controllerOptions, $arguments);
if ($arguments !== false) {
$request->setRouteParams($controllerOptions);
// controllerOptions provide some default arguments
$arguments = array_merge($controllerOptions, $arguments);
// Pop additional tokens from the tokenizer if necessary // Pop additional tokens from the tokenizer if necessary
if (isset($controllerOptions['_PopTokeniser'])) { if (isset($controllerOptions['_PopTokeniser'])) {
$request->shift($controllerOptions['_PopTokeniser']); $request->shift($controllerOptions['_PopTokeniser']);
} }
// Handler for redirection // Handler for redirection
if (isset($arguments['Redirect'])) { if (isset($arguments['Redirect'])) {
$handler = function () use ($arguments) { $handler = function () use ($arguments) {
// Redirection // Redirection
$response = new HTTPResponse(); $response = new HTTPResponse();
$response->redirect(static::absoluteURL($arguments['Redirect'])); $response->redirect(static::absoluteURL($arguments['Redirect']));
return $response; return $response;
};
break;
}
// Find the controller name
$controller = $arguments['Controller'];
// String = service name
if (is_string($controller)) {
$controllerObj = Injector::inst()->get($controller);
// Array = service spec
} elseif (is_array($controller)) {
$controllerObj = Injector::inst()->createFromSpec($controller);
} else {
throw new \LogicException("Invalid Controller value '$controller'");
}
// Handler for calling a controller
$handler = function ($request) use ($controllerObj) {
try {
// Apply the controller's middleware. We do this outside of handleRequest so that
// subclasses of handleRequest will be called after the middlware processing
return $controllerObj->callMiddleware($request, function ($request) use ($controllerObj) {
return $controllerObj->handleRequest($request);
});
} catch (HTTPResponse_Exception $responseException) {
return $responseException->getResponse();
}
}; };
break; break;
} }
/** @var RequestHandler $controllerObj */
$controllerObj = Injector::inst()->create($arguments['Controller']);
// Handler for calling a controller
$handler = function (HTTPRequest $request) use ($controllerObj) {
try {
return $controllerObj->handleRequest($request);
} catch (HTTPResponse_Exception $responseException) {
return $responseException->getResponse();
}
};
break;
} }
// Call the handler with the configured middlewares // Call the handler with the configured middlewares
@ -381,43 +372,11 @@ class Director implements TemplateGlobalProvider
// Note that if a different request was previously registered, this will now be lost // Note that if a different request was previously registered, this will now be lost
// In these cases it's better to use Kernel::nest() prior to kicking off a nested request // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
Injector::inst()->unregisterNamedObject(HTTPRequest::class); Injector::inst()->unregisterNamedObject(HTTPRequest::class, true);
return $response; return $response;
} }
/**
* Call the given request handler with the given middlewares
* Middlewares are specified as Injector service names
*
* @param $request The request to pass to the handler
* @param $middlewareNames The services names of the middlewares to apply
* @param $handler The request handler
*/
protected static function callWithMiddlewares(HTTPRequest $request, array $middlewareNames, callable $handler)
{
$next = $handler;
if ($middlewareNames) {
$middlewares = array_map(
function ($name) {
return Injector::inst()->get($name);
},
$middlewareNames
);
// Reverse middlewares
/** @var HTTPMiddleware $middleware */
foreach (array_reverse($middlewares) as $middleware) {
$next = function ($request) use ($middleware, $next) {
return $middleware->process($request, $next);
};
}
}
return $next($request);
}
/** /**
* Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
* object to return, then this will return the current controller. * object to return, then this will return the current controller.
@ -502,6 +461,7 @@ class Director implements TemplateGlobalProvider
* - SERVER_NAME * - SERVER_NAME
* - gethostname() * - gethostname()
* *
* @param HTTPRequest $request
* @return string * @return string
*/ */
public static function host(HTTPRequest $request = null) public static function host(HTTPRequest $request = null)
@ -515,10 +475,8 @@ class Director implements TemplateGlobalProvider
} }
} }
if (!$request) { $request = static::currentRequest($request);
$request = Injector::inst()->get(HTTPRequest::class, true, ['GET', '/']); if ($request && ($host = $request->getHeader('Host'))) {
}
if ($request && $host = $request->getHeader('Host')) {
return $host; return $host;
} }
@ -544,6 +502,7 @@ class Director implements TemplateGlobalProvider
* Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
* variable isn't set. * variable isn't set.
* *
* @param HTTPRequest $request
* @return bool|string * @return bool|string
*/ */
public static function protocolAndHost(HTTPRequest $request = null) public static function protocolAndHost(HTTPRequest $request = null)
@ -554,6 +513,7 @@ class Director implements TemplateGlobalProvider
/** /**
* Return the current protocol that the site is running under. * Return the current protocol that the site is running under.
* *
* @param HTTPRequest $request
* @return string * @return string
*/ */
public static function protocol(HTTPRequest $request = null) public static function protocol(HTTPRequest $request = null)
@ -564,6 +524,7 @@ class Director implements TemplateGlobalProvider
/** /**
* Return whether the site is running as under HTTPS. * Return whether the site is running as under HTTPS.
* *
* @param HTTPRequest $request
* @return bool * @return bool
*/ */
public static function is_https(HTTPRequest $request = null) public static function is_https(HTTPRequest $request = null)
@ -578,11 +539,9 @@ class Director implements TemplateGlobalProvider
} }
// Check the current request // Check the current request
if (!$request) { $request = static::currentRequest($request);
$request = Injector::inst()->get(HTTPRequest::class, true, ['GET', '/']); if ($request && ($scheme = $request->getScheme())) {
} return $scheme === 'https';
if ($request && $host = $request->getHeader('Host')) {
return $request->getScheme() === 'https';
} }
// Check default_base_url // Check default_base_url
@ -842,9 +801,10 @@ class Director implements TemplateGlobalProvider
* Returns the Absolute URL of the site root, embedding the current basic-auth credentials into * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
* the URL. * the URL.
* *
* @param HTTPRequest|null $request
* @return string * @return string
*/ */
public static function absoluteBaseURLWithAuth() public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
{ {
$login = ""; $login = "";
@ -852,7 +812,7 @@ class Director implements TemplateGlobalProvider
$login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@"; $login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@";
} }
return Director::protocol() . $login . static::host() . Director::baseURL(); return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
} }
/** /**
@ -960,12 +920,14 @@ class Director implements TemplateGlobalProvider
* Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
* jQuery or whether a manually set request-parameter 'ajax' is present. * jQuery or whether a manually set request-parameter 'ajax' is present.
* *
* @param HTTPRequest $request
* @return bool * @return bool
*/ */
public static function is_ajax() public static function is_ajax(HTTPRequest $request = null)
{ {
if (Controller::has_curr()) { $request = self::currentRequest($request);
return Controller::curr()->getRequest()->isAjax(); if ($request) {
return $request->isAjax();
} else { } else {
return ( return (
isset($_REQUEST['ajax']) || isset($_REQUEST['ajax']) ||
@ -1046,4 +1008,20 @@ class Director implements TemplateGlobalProvider
'BaseHref' => 'absoluteBaseURL', //@deprecated 3.0 'BaseHref' => 'absoluteBaseURL', //@deprecated 3.0
); );
} }
/**
* Helper to validate or check the current request object
*
* @param HTTPRequest $request
* @return HTTPRequest Request object if one is both current and valid
*/
protected static function currentRequest(HTTPRequest $request = null)
{
// Ensure we only use a registered HTTPRequest and don't
// incidentally construct a singleton
if (!$request && Injector::inst()->has(HTTPRequest::class)) {
$request = Injector::inst()->get(HTTPRequest::class);
}
return $request;
}
} }

View File

@ -2,9 +2,8 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
use SilverStripe\Core\Application; use SilverStripe\Core\Application;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\HTTPMiddleware;
use SilverStripe\Core\Kernel; use SilverStripe\Core\Kernel;
/** /**
@ -12,7 +11,6 @@ use SilverStripe\Core\Kernel;
*/ */
class HTTPApplication implements Application class HTTPApplication implements Application
{ {
use HTTPMiddlewareAware; use HTTPMiddlewareAware;
/** /**
@ -47,7 +45,7 @@ class HTTPApplication implements Application
// Ensure boot is invoked // Ensure boot is invoked
return $this->execute($request, function (HTTPRequest $request) { return $this->execute($request, function (HTTPRequest $request) {
return Injector::inst()->get(Director::class)->handleRequest($request); return Director::singleton()->handleRequest($request);
}, $flush); }, $flush);
} }

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Control;
use ArrayAccess; use ArrayAccess;
use BadMethodCallException; use BadMethodCallException;
use InvalidArgumentException;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\ORM\ArrayLib; use SilverStripe\ORM\ArrayLib;
@ -783,12 +784,18 @@ class HTTPRequest implements ArrayAccess
/** /**
* Sets the client IP address which originated this request. * Sets the client IP address which originated this request.
* Use setIPFromHeaderValue if assigning from header value.
* *
* @param $ip string * @param $ip string
* @return $this
*/ */
public function setIP($ip) public function setIP($ip)
{ {
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
throw new InvalidArgumentException("Invalid ip $ip");
}
$this->ip = $ip; $this->ip = $ip;
return $this;
} }
/** /**
@ -820,9 +827,11 @@ class HTTPRequest implements ArrayAccess
/** /**
* Return the URL scheme (e.g. "http" or "https"). * Return the URL scheme (e.g. "http" or "https").
* Equivalent to PSR-7 getUri()->getScheme() * Equivalent to PSR-7 getUri()->getScheme()
*
* @return string * @return string
*/ */
public function getScheme() { public function getScheme()
{
return $this->scheme; return $this->scheme;
} }
@ -831,9 +840,12 @@ class HTTPRequest implements ArrayAccess
* Equivalent to PSR-7 getUri()->getScheme(), * Equivalent to PSR-7 getUri()->getScheme(),
* *
* @param string $scheme * @param string $scheme
* @return $this
*/ */
public function setScheme($scheme) { public function setScheme($scheme)
{
$this->scheme = $scheme; $this->scheme = $scheme;
return $this;
} }
/** /**

View File

@ -1,17 +1,25 @@
<?php <?php
namespace SilverStripe\Control; namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
/** /**
* Secures requests by only allowing a whitelist of Host values * Secures requests by only allowing a whitelist of Host values
*/ */
class AllowedHostsMiddleware implements HTTPMiddleware class AllowedHostsMiddleware implements HTTPMiddleware
{ {
/**
private $allowedHosts = null; * List of allowed hosts
*
* @var array
*/
private $allowedHosts = [];
/** /**
* @return string A comma-separted list of allowed Host header values * @return array List of allowed Host header values
*/ */
public function getAllowedHosts() public function getAllowedHosts()
{ {
@ -19,11 +27,19 @@ class AllowedHostsMiddleware implements HTTPMiddleware
} }
/** /**
* @param $allowedHosts string A comma-separted list of allowed Host header values * Sets the list of allowed Host header values
* Can also specify a comma separated list
*
* @param array|string $allowedHosts
* @return $this
*/ */
public function setAllowedHosts($allowedHosts) public function setAllowedHosts($allowedHosts)
{ {
if (is_string($allowedHosts)) {
$allowedHosts = preg_split('/ *, */', $allowedHosts);
}
$this->allowedHosts = $allowedHosts; $this->allowedHosts = $allowedHosts;
return $this;
} }
/** /**
@ -31,13 +47,14 @@ class AllowedHostsMiddleware implements HTTPMiddleware
*/ */
public function process(HTTPRequest $request, callable $delegate) public function process(HTTPRequest $request, callable $delegate)
{ {
if ($this->allowedHosts && !Director::is_cli()) { $allowedHosts = $this->getAllowedHosts();
$allowedHosts = preg_split('/ *, */', $this->allowedHosts);
// check allowed hosts // check allowed hosts
if (!in_array($request->getHeader('Host'), $allowedHosts)) { if ($allowedHosts
return new HTTPResponse('Invalid Host', 400); && !Director::is_cli()
} && !in_array($request->getHeader('Host'), $allowedHosts)
) {
return new HTTPResponse('Invalid Host', 400);
} }
return $delegate($request); return $delegate($request);

View File

@ -1,9 +1,10 @@
<?php <?php
namespace SilverStripe\Control; namespace SilverStripe\Control\Middleware;
use SilverStripe\Core\Flushable; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Flushable;
/** /**
* Triggers a call to flush() on all implementors of Flushable. * Triggers a call to flush() on all implementors of Flushable.
@ -17,6 +18,7 @@ class FlushMiddleware implements HTTPMiddleware
{ {
if (array_key_exists('flush', $request->getVars())) { if (array_key_exists('flush', $request->getVars())) {
foreach (ClassInfo::implementorsOf(Flushable::class) as $class) { foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
/** @var Flushable|string $class */
$class::flush(); $class::flush();
} }
} }

View File

@ -1,6 +1,9 @@
<?php <?php
namespace SilverStripe\Control; namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
/** /**
* HTTP Request middleware * HTTP Request middleware

View File

@ -1,6 +1,9 @@
<?php <?php
namespace SilverStripe\Control; namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
/** /**
* Adds middleware support to an object. * Adds middleware support to an object.
@ -45,11 +48,11 @@ trait HTTPMiddlewareAware
/** /**
* Call middleware * Call middleware
* *
* @param $request The request to pass to the middlewares and callback * @param HTTPRequest $request The request to pass to the middlewares and callback
* @param $last The callback to call after all middlewares * @param callable $last The callback to call after all middlewares
* @return HTTPResponse * @return HTTPResponse
*/ */
public function callMiddleware(HTTPRequest $request, callable $last) protected function callMiddleware(HTTPRequest $request, callable $last)
{ {
// Reverse middlewares // Reverse middlewares
$next = $last; $next = $last;

View File

@ -0,0 +1,59 @@
<?php
namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Dev\Debug;
/**
* Decorates a request handler with the HTTP Middleware pattern
*/
class RequestHandlerMiddlewareAdapter extends RequestHandler
{
use HTTPMiddlewareAware;
/**
* @var RequestHandler
*/
protected $requestHandler = null;
public function __construct(RequestHandler $handler = null)
{
if ($handler) {
$this->setRequestHandler($handler);
}
parent::__construct();
}
public function Link($action = null)
{
return $this->getRequestHandler()->Link($action);
}
/**
* @return RequestHandler
*/
public function getRequestHandler()
{
return $this->requestHandler;
}
/**
* @param RequestHandler $requestHandler
* @return $this
*/
public function setRequestHandler(RequestHandler $requestHandler)
{
$this->requestHandler = $requestHandler;
return $this;
}
public function handleRequest(HTTPRequest $request)
{
return $this->callMiddleware($request, function (HTTPRequest $request) {
$this->setRequest($request);
return $this->getRequestHandler()->handleRequest($request);
});
}
}

View File

@ -1,6 +1,8 @@
<?php <?php
namespace SilverStripe\Control; namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\HTTPRequest;
class SessionMiddleware implements HTTPMiddleware class SessionMiddleware implements HTTPMiddleware
{ {

View File

@ -1,24 +1,47 @@
<?php <?php
namespace SilverStripe\Control; namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Util\IPUtils;
/** /**
* This middleware will rewrite headers that provide IP and host details from an upstream proxy. * This middleware will rewrite headers that provide IP and host details from an upstream proxy.
*/ */
class TrustedProxyMiddleware implements HTTPMiddleware class TrustedProxyMiddleware implements HTTPMiddleware
{ {
/**
* Comma-separated list of IP ranges that are trusted to provide proxy headers.
* Can also be 'none' or '*' (all)
*
* @var string
*/
private $trustedProxyIPs = null; private $trustedProxyIPs = null;
/**
* Array of headers from which to lookup the hostname
*
* @var array
*/
private $proxyHostHeaders = [ private $proxyHostHeaders = [
'X-Forwarded-Host' 'X-Forwarded-Host'
]; ];
/**
* Array of headers from which to lookup the client IP
*
* @var array
*/
private $proxyIPHeaders = [ private $proxyIPHeaders = [
'Client-IP', 'Client-IP',
'X-Forwarded-For' 'X-Forwarded-For'
]; ];
/**
* Array of headers from which to lookup the client scheme (http/https)
*
* @var array
*/
private $proxySchemeHeaders = [ private $proxySchemeHeaders = [
'X-Forwarded-Protocol', 'X-Forwarded-Protocol',
'X-Forwarded-Proto', 'X-Forwarded-Proto',
@ -26,6 +49,7 @@ class TrustedProxyMiddleware implements HTTPMiddleware
/** /**
* Return the comma-separated list of IP ranges that are trusted to provide proxy headers * Return the comma-separated list of IP ranges that are trusted to provide proxy headers
* Can also be 'none' or '*' (all)
* *
* @return string * @return string
*/ */
@ -36,18 +60,21 @@ class TrustedProxyMiddleware implements HTTPMiddleware
/** /**
* Set the comma-separated list of IP ranges that are trusted to provide proxy headers * Set the comma-separated list of IP ranges that are trusted to provide proxy headers
* Can also be 'none' or '*' (all)
* *
* @param $trustedProxyIPs string * @param string $trustedProxyIPs
* @return $this
*/ */
public function setTrustedProxyIPs($trustedProxyIPs) public function setTrustedProxyIPs($trustedProxyIPs)
{ {
$this->trustedProxyIPs = $trustedProxyIPs; $this->trustedProxyIPs = $trustedProxyIPs;
return $this;
} }
/** /**
* Return the comma-separated list of headers from which to lookup the hostname * Return the array of headers from which to lookup the hostname
* *
* @return string * @return array
*/ */
public function getProxyHostHeaders() public function getProxyHostHeaders()
{ {
@ -55,19 +82,25 @@ class TrustedProxyMiddleware implements HTTPMiddleware
} }
/** /**
* Set the comma-separated list of headers from which to lookup the hostname * Set the array of headers from which to lookup the hostname
* Can also specify comma-separated list as a single string.
* *
* @param $proxyHostHeaders string * @param array|string $proxyHostHeaders
* @return $this
*/ */
public function setProxyHostHeaders($proxyHostHeaders) public function setProxyHostHeaders($proxyHostHeaders)
{ {
$this->proxyHostHeaders = $proxyHostHeaders; if (is_string($proxyHostHeaders)) {
$proxyHostHeaders = preg_split('/ *, */', $proxyHostHeaders);
}
$this->proxyHostHeaders = $proxyHostHeaders ?: [];
return $this;
} }
/** /**
* Return the comma-separated list of headers from which to lookup the client IP * Return the array of headers from which to lookup the client IP
* *
* @return string * @return array
*/ */
public function getProxyIPHeaders() public function getProxyIPHeaders()
{ {
@ -75,19 +108,25 @@ class TrustedProxyMiddleware implements HTTPMiddleware
} }
/** /**
* Set the comma-separated list of headers from which to lookup the client IP * Set the array of headers from which to lookup the client IP
* Can also specify comma-separated list as a single string.
* *
* @param $proxyIPHeaders string * @param array|string $proxyIPHeaders
* @return $this
*/ */
public function setProxyIPHeaders($proxyIPHeaders) public function setProxyIPHeaders($proxyIPHeaders)
{ {
$this->proxyIPHeaders = $proxyIPHeaders; if (is_string($proxyIPHeaders)) {
$proxyIPHeaders = preg_split('/ *, */', $proxyIPHeaders);
}
$this->proxyIPHeaders = $proxyIPHeaders ?: [];
return $this;
} }
/** /**
* Return the comma-separated list of headers from which to lookup the client scheme (http/https) * Return the array of headers from which to lookup the client scheme (http/https)
* *
* @return string * @return array
*/ */
public function getProxySchemeHeaders() public function getProxySchemeHeaders()
{ {
@ -95,13 +134,19 @@ class TrustedProxyMiddleware implements HTTPMiddleware
} }
/** /**
* Set the comma-separated list of headers from which to lookup the client scheme (http/https) * Set array of headers from which to lookup the client scheme (http/https)
* Can also specify comma-separated list as a single string.
* *
* @param $proxySchemeHeaders string * @param array|string $proxySchemeHeaders
* @return $this
*/ */
public function setProxySchemeHeaders($proxySchemeHeaders) public function setProxySchemeHeaders($proxySchemeHeaders)
{ {
$this->proxySchemeHeaders = $proxySchemeHeaders; if (is_string($proxySchemeHeaders)) {
$proxySchemeHeaders = preg_split('/ *, */', $proxySchemeHeaders);
}
$this->proxySchemeHeaders = $proxySchemeHeaders ?: [];
return $this;
} }
public function process(HTTPRequest $request, callable $delegate) public function process(HTTPRequest $request, callable $delegate)
@ -109,29 +154,32 @@ class TrustedProxyMiddleware implements HTTPMiddleware
// If this is a trust proxy // If this is a trust proxy
if ($this->isTrustedProxy($request)) { if ($this->isTrustedProxy($request)) {
// Replace host // Replace host
foreach ($this->proxyHostHeaders as $header) { foreach ($this->getProxyHostHeaders() as $header) {
$hostList = $request->getHeader($header); $hostList = $request->getHeader($header);
if ($hostList) { if ($hostList) {
$request->setHeader('Host', strtok($hostList, ',')); $request->addHeader('Host', strtok($hostList, ','));
break; break;
} }
} }
// Replace scheme // Replace scheme
foreach ($this->proxySchemeHeaders as $header) { foreach ($this->getProxySchemeHeaders() as $header) {
$scheme = $request->getHeader($header); $headerValue = $request->getHeader($header);
if ($scheme) { if ($headerValue) {
$request->setScheme(strtolower($scheme)); $request->setScheme(strtolower($headerValue));
break; break;
} }
} }
// Replace IP // Replace IP
foreach ($this->proxyIPHeaders as $header) { foreach ($this->proxyIPHeaders as $header) {
$ipHeader = $this->getIPFromHeaderValue($request->getHeader($header)); $headerValue = $request->getHeader($header);
if ($ipHeader) { if ($headerValue) {
$request->setIP($ipHeader); $ipHeader = $this->getIPFromHeaderValue($headerValue);
break; if ($ipHeader) {
$request->setIP($ipHeader);
break;
}
} }
} }
} }
@ -142,12 +190,15 @@ class TrustedProxyMiddleware implements HTTPMiddleware
/** /**
* Determine if the current request is coming from a trusted proxy * Determine if the current request is coming from a trusted proxy
* *
* @return boolean True if the request's source IP is a trusted proxy * @param HTTPRequest $request
* @return bool True if the request's source IP is a trusted proxy
*/ */
protected function isTrustedProxy($request) protected function isTrustedProxy(HTTPRequest $request)
{ {
$trustedIPs = $this->getTrustedProxyIPs();
// Disabled // Disabled
if (empty($this->trustedProxyIPs) || $trustedIPs === 'none') { if (empty($trustedIPs) || $trustedIPs === 'none') {
return false; return false;
} }
@ -157,8 +208,9 @@ class TrustedProxyMiddleware implements HTTPMiddleware
} }
// Validate IP address // Validate IP address
if ($ip = $request->getIP()) { $ip = $request->getIP();
return IPUtils::checkIP($ip, explode(',', $trustedIPs)); if ($ip) {
return IPUtils::checkIP($ip, preg_split('/ *, */', $trustedIPs));
} }
return false; return false;
@ -173,19 +225,24 @@ class TrustedProxyMiddleware implements HTTPMiddleware
*/ */
protected function getIPFromHeaderValue($headerValue) protected function getIPFromHeaderValue($headerValue)
{ {
if (strpos($headerValue, ',') !== false) { // Sometimes the IP from a load balancer could be "x.x.x.x, y.y.y.y, z.z.z.z"
//sometimes the IP from a load balancer could be "x.x.x.x, y.y.y.y, z.z.z.z" so we need to find the most // so we need to find the most likely candidate
// likely candidate $ips = preg_split('/\s*,\s*/', $headerValue);
$ips = explode(',', $headerValue);
// Prioritise filters
$filters = [
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
FILTER_FLAG_NO_PRIV_RANGE,
null
];
foreach ($filters as $filter) {
// Find best IP
foreach ($ips as $ip) { foreach ($ips as $ip) {
$ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, $filter)) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip; return $ip;
} else {
return null;
} }
} }
} }
return $headerValue; return null;
} }
} }

View File

@ -9,6 +9,7 @@ namespace SilverStripe\Control;
* *
* @author marcus@silverstripe.com.au * @author marcus@silverstripe.com.au
* @license BSD License http://silverstripe.org/bsd-license/ * @license BSD License http://silverstripe.org/bsd-license/
* @deprecated 4.0..5.0 Use HTTPMiddleware instead
*/ */
interface RequestFilter interface RequestFilter
{ {

View File

@ -2,17 +2,17 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use BadMethodCallException;
use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use ReflectionClass;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Security\Security;
use SilverStripe\Security\PermissionFailureException;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\PermissionFailureException;
use SilverStripe\Security\Security;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
use ReflectionClass;
use Exception;
use BadMethodCallException;
/** /**
* This class is the base class of any SilverStripe object that can be used to handle HTTP requests. * This class is the base class of any SilverStripe object that can be used to handle HTTP requests.
@ -47,8 +47,6 @@ use BadMethodCallException;
class RequestHandler extends ViewableData class RequestHandler extends ViewableData
{ {
use HTTPMiddlewareAware;
/** /**
* Optional url_segment for this request handler * Optional url_segment for this request handler
* *

View File

@ -2,12 +2,14 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
/** /**
* Middleware that provides back-support for the deprecated RequestFilter API. * Middleware that provides back-support for the deprecated RequestFilter API.
* You should use HTTPMiddleware directly instead. *
* @deprecated 4.0..5.0 Use HTTPMiddleware directly instead.
*/ */
class RequestProcessor implements HTTPMiddleware class RequestProcessor implements HTTPMiddleware
{ {
@ -16,10 +18,15 @@ class RequestProcessor implements HTTPMiddleware
/** /**
* List of currently assigned request filters * List of currently assigned request filters
* *
* @var array * @var RequestFilter[]
*/ */
private $filters = array(); private $filters = array();
/**
* Construct new RequestFilter with a list of filter objects
*
* @param RequestFilter[] $filters
*/
public function __construct($filters = array()) public function __construct($filters = array())
{ {
$this->filters = $filters; $this->filters = $filters;
@ -28,11 +35,13 @@ class RequestProcessor implements HTTPMiddleware
/** /**
* Assign a list of request filters * Assign a list of request filters
* *
* @param array $filters * @param RequestFilter[] $filters
* @return $this
*/ */
public function setFilters($filters) public function setFilters($filters)
{ {
$this->filters = $filters; $this->filters = $filters;
return $this;
} }
/** /**
@ -42,7 +51,7 @@ class RequestProcessor implements HTTPMiddleware
{ {
if ($this->filters) { if ($this->filters) {
Deprecation::notice( Deprecation::notice(
'4.0', '5.0',
'Deprecated RequestFilters are in use. Apply HTTPMiddleware to Director.middlewares instead.' 'Deprecated RequestFilters are in use. Apply HTTPMiddleware to Director.middlewares instead.'
); );
} }

View File

@ -143,9 +143,10 @@ class Session
/** /**
* Get user agent for this request * Get user agent for this request
* *
* @param HTTPRequest $request
* @return string * @return string
*/ */
protected function userAgent($request) protected function userAgent(HTTPRequest $request)
{ {
return $request->getHeader('User-Agent'); return $request->getHeader('User-Agent');
} }
@ -167,6 +168,8 @@ class Session
/** /**
* Init this session instance before usage * Init this session instance before usage
*
* @param HTTPRequest $request
*/ */
public function init(HTTPRequest $request) public function init(HTTPRequest $request)
{ {
@ -186,6 +189,8 @@ class Session
/** /**
* Destroy existing session and restart * Destroy existing session and restart
*
* @param HTTPRequest $request
*/ */
public function restart(HTTPRequest $request) public function restart(HTTPRequest $request)
{ {
@ -206,7 +211,7 @@ class Session
/** /**
* Begin session * Begin session
* *
* @param $request The request for which to start a session * @param HTTPRequest $request The request for which to start a session
*/ */
public function start(HTTPRequest $request) public function start(HTTPRequest $request)
{ {
@ -463,6 +468,8 @@ class Session
/** /**
* Set user agent key * Set user agent key
*
* @param HTTPRequest $request
*/ */
public function finalize(HTTPRequest $request) public function finalize(HTTPRequest $request)
{ {
@ -472,6 +479,8 @@ class Session
/** /**
* Save data to session * Save data to session
* Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned. * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
*
* @param HTTPRequest $request
*/ */
public function save(HTTPRequest $request) public function save(HTTPRequest $request)
{ {

View File

@ -2,6 +2,8 @@
namespace SilverStripe\Core; namespace SilverStripe\Core;
use SilverStripe\Control\Middleware\FlushMiddleware;
/** /**
* Provides an interface for classes to implement their own flushing functionality * Provides an interface for classes to implement their own flushing functionality
* whenever flush=1 is requested. * whenever flush=1 is requested.

View File

@ -851,11 +851,15 @@ class Injector implements ContainerInterface
* by the inject * by the inject
* *
* @param string $name The name to unregister * @param string $name The name to unregister
* @param bool $flushSpecs Set to true to clear spec for this service
* @return $this * @return $this
*/ */
public function unregisterNamedObject($name) public function unregisterNamedObject($name, $flushSpecs = false)
{ {
unset($this->serviceCache[$name]); unset($this->serviceCache[$name]);
if ($flushSpecs) {
unset($this->specs[$name]);
}
return $this; return $this;
} }
@ -863,9 +867,10 @@ class Injector implements ContainerInterface
* Clear out objects of one or more types that are managed by the injetor. * Clear out objects of one or more types that are managed by the injetor.
* *
* @param array|string $types Base class of object (not service name) to remove * @param array|string $types Base class of object (not service name) to remove
* @param bool $flushSpecs Set to true to clear spec for this service
* @return $this * @return $this
*/ */
public function unregisterObjects($types) public function unregisterObjects($types, $flushSpecs = false)
{ {
if (!is_array($types)) { if (!is_array($types)) {
$types = [ $types ]; $types = [ $types ];
@ -879,7 +884,7 @@ class Injector implements ContainerInterface
throw new InvalidArgumentException("Global unregistration is not allowed"); throw new InvalidArgumentException("Global unregistration is not allowed");
} }
if ($object instanceof $filterClass) { if ($object instanceof $filterClass) {
unset($this->serviceCache[$key]); $this->unregisterNamedObject($key, $flushSpecs);
break; break;
} }
} }

View File

@ -7,7 +7,7 @@ use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Application; use SilverStripe\Core\Application;
use SilverStripe\Control\HTTPMiddleware; use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;

View File

@ -4,8 +4,7 @@ namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Control\HTTPMiddleware;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
@ -40,8 +39,8 @@ class AuthenticationMiddleware implements HTTPMiddleware
* Identify the current user from the request * Identify the current user from the request
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @return bool|void * @param callable $delegate
* @throws HTTPResponse_Exception * @return HTTPResponse
*/ */
public function process(HTTPRequest $request, callable $delegate) public function process(HTTPRequest $request, callable $delegate)
{ {
@ -60,4 +59,4 @@ class AuthenticationMiddleware implements HTTPMiddleware
return $delegate($request); return $delegate($request);
} }
} }

View File

@ -7,12 +7,15 @@ use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPRequestBuilder; use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter;
use SilverStripe\Control\Middleware\TrustedProxyMiddleware;
use SilverStripe\Control\RequestProcessor; use SilverStripe\Control\RequestProcessor;
use SilverStripe\Control\Tests\DirectorTest\TestController; use SilverStripe\Control\Tests\DirectorTest\TestController;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel; use SilverStripe\Core\Kernel;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Core\Config\Config;
/** /**
* @todo test Director::alternateBaseFolder() * @todo test Director::alternateBaseFolder()
@ -535,63 +538,91 @@ class DirectorTest extends SapphireTest
public function testIsHttps() public function testIsHttps()
{ {
if (!TRUSTED_PROXY) { // Trust all IPs for this test
$this->markTestSkipped('Test cannot be run without trusted proxy'); /** @var TrustedProxyMiddleware $trustedProxyMiddleware */
} $trustedProxyMiddleware
= Injector::inst()->get(TrustedProxyMiddleware::class);
$trustedProxyMiddleware->setTrustedProxyIPs('*');
// Clear alternate_base_url for this test
Director::config()->remove('alternate_base_url');
// nothing available // nothing available
$headers = array( $headers = array(
'HTTP_X_FORWARDED_PROTOCOL', 'HTTPS', 'SSL' 'HTTP_X_FORWARDED_PROTOCOL', 'HTTPS', 'SSL'
); );
$origServer = $_SERVER;
foreach ($headers as $header) { foreach ($headers as $header) {
if (isset($_SERVER[$header])) { if (isset($_SERVER[$header])) {
unset($_SERVER['HTTP_X_FORWARDED_PROTOCOL']); unset($_SERVER['HTTP_X_FORWARDED_PROTOCOL']);
} }
} }
$this->assertFalse(Director::is_https()); $this->assertEquals(
'no',
Director::test('TestController/returnIsSSL')->getBody()
);
$_SERVER['HTTP_X_FORWARDED_PROTOCOL'] = 'https'; $this->assertEquals(
$this->assertTrue(Director::is_https()); 'yes',
Director::test(
'TestController/returnIsSSL',
null,
null,
null,
null,
[ 'X-Forwarded-Protocol' => 'https' ]
)->getBody()
);
$_SERVER['HTTP_X_FORWARDED_PROTOCOL'] = 'http'; $this->assertEquals(
$this->assertFalse(Director::is_https()); 'no',
Director::test(
'TestController/returnIsSSL',
null,
null,
null,
null,
[ 'X-Forwarded-Protocol' => 'http' ]
)->getBody()
);
$_SERVER['HTTP_X_FORWARDED_PROTOCOL'] = 'ftp'; $this->assertEquals(
$this->assertFalse(Director::is_https()); 'no',
Director::test(
$_SERVER['HTTP_X_FORWARDED_PROTO'] = 'https'; 'TestController/returnIsSSL',
$this->assertTrue(Director::is_https()); null,
null,
$_SERVER['HTTP_X_FORWARDED_PROTO'] = 'http'; null,
$this->assertFalse(Director::is_https()); null,
[ 'X-Forwarded-Protocol' => 'ftp' ]
$_SERVER['HTTP_X_FORWARDED_PROTO'] = 'ftp'; )->getBody()
$this->assertFalse(Director::is_https()); );
$_SERVER['HTTP_FRONT_END_HTTPS'] = 'On';
$this->assertTrue(Director::is_https());
$_SERVER['HTTP_FRONT_END_HTTPS'] = 'Off';
$this->assertFalse(Director::is_https());
// https via HTTPS // https via HTTPS
$_SERVER['HTTPS'] = 'true'; $_SERVER['HTTPS'] = 'true';
$this->assertTrue(Director::is_https()); $this->assertEquals(
'yes',
Director::test('TestController/returnIsSSL')->getBody()
);
$_SERVER['HTTPS'] = '1'; $_SERVER['HTTPS'] = '1';
$this->assertTrue(Director::is_https()); $this->assertEquals(
'yes',
Director::test('TestController/returnIsSSL')->getBody()
);
$_SERVER['HTTPS'] = 'off'; $_SERVER['HTTPS'] = 'off';
$this->assertFalse(Director::is_https()); $this->assertEquals(
'no',
Director::test('TestController/returnIsSSL')->getBody()
);
// https via SSL // https via SSL
$_SERVER['SSL'] = ''; $_SERVER['SSL'] = '';
$this->assertTrue(Director::is_https()); $this->assertEquals(
'yes',
$_SERVER = $origServer; Director::test('TestController/returnIsSSL')->getBody()
);
} }
public function testTestIgnoresHashes() public function testTestIgnoresHashes()
@ -650,9 +681,7 @@ class DirectorTest extends SapphireTest
public function testGlobalMiddleware() public function testGlobalMiddleware()
{ {
$middleware = new DirectorTest\TestMiddleware; $middleware = new DirectorTest\TestMiddleware;
Director::singleton()->setMiddlewares([$middleware]);
Injector::inst()->registerService($middleware, 'Middleware1');
Config::modify()->set(Director::class, 'middlewares', [ 'Middleware1' ]);
$response = Director::test('some-dummy-url'); $response = Director::test('some-dummy-url');
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
@ -682,14 +711,30 @@ class DirectorTest extends SapphireTest
public function testRouteSpecificMiddleware() public function testRouteSpecificMiddleware()
{ {
$middleware = new DirectorTest\TestMiddleware; // Inject adapter in place of controller
$specificMiddleware = new DirectorTest\TestMiddleware; $specificMiddleware = new DirectorTest\TestMiddleware;
Injector::inst()->registerService($specificMiddleware, 'SpecificMiddleware');
Injector::inst()->registerService($middleware, 'Middleware1'); // Register adapter as factory for creating this controller
Injector::inst()->registerService($specificMiddleware, 'Middleware2'); Config::modify()->merge(
Injector::class,
'ControllerWithMiddleware',
[
'class' => RequestHandlerMiddlewareAdapter::class,
'constructor' => [
'%$' . TestController::class
],
'properties' => [
'Middlewares' => [
'%$SpecificMiddleware',
],
],
]
);
// Global middleware // Global middleware
Config::modify()->set(Director::class, 'middlewares', [ 'Middleware1' ]); $middleware = new DirectorTest\TestMiddleware;
Director::singleton()->setMiddlewares([ $middleware ]);
// URL rules, one of which has a specific middleware // URL rules, one of which has a specific middleware
Config::modify()->set( Config::modify()->set(
@ -698,24 +743,22 @@ class DirectorTest extends SapphireTest
[ [
'url-one' => TestController::class, 'url-one' => TestController::class,
'url-two' => [ 'url-two' => [
'Controller' => TestController::class, 'Controller' => 'ControllerWithMiddleware',
'Middlewares' => [ 'Middleware2' ] ],
]
] ]
); );
// URL without a route-specific middleware // URL without a route-specific middleware
$response = Director::test('url-one'); Director::test('url-one');
// Only the global middleware triggered // Only the global middleware triggered
$this->assertEquals(1, $middleware->preCalls); $this->assertEquals(1, $middleware->preCalls);
$this->assertEquals(0, $specificMiddleware->postCalls); $this->assertEquals(0, $specificMiddleware->postCalls);
$response = Director::test('url-two'); Director::test('url-two');
// Both triggered on the url with the specific middleware applied // Both triggered on the url with the specific middleware applied
$this->assertEquals(2, $middleware->preCalls); $this->assertEquals(2, $middleware->preCalls);
$this->assertEquals(1, $specificMiddleware->postCalls); $this->assertEquals(1, $specificMiddleware->postCalls);
} }
} }

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Control\Tests\DirectorTest; namespace SilverStripe\Control\Tests\DirectorTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
@ -22,6 +23,7 @@ class TestController extends Controller implements TestOnly
'returnPostValue', 'returnPostValue',
'returnRequestValue', 'returnRequestValue',
'returnCookieValue', 'returnCookieValue',
'returnIsSSL',
); );
public function returnGetValue($request) public function returnGetValue($request)
@ -55,4 +57,9 @@ class TestController extends Controller implements TestOnly
} }
return null; return null;
} }
public function returnIsSSL()
{
return Director::is_https() ? 'yes': 'no';
}
} }

View File

@ -4,7 +4,7 @@ namespace SilverStripe\Control\Tests\DirectorTest;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPMiddleware; use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
class TestMiddleware implements HTTPMiddleware, TestOnly class TestMiddleware implements HTTPMiddleware, TestOnly

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use SilverStripe\Control\Middleware\TrustedProxyMiddleware;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use ReflectionMethod; use ReflectionMethod;
@ -267,9 +268,9 @@ class HTTPRequestTest extends SapphireTest
$this->assertEquals('home', $req->getURL()); $this->assertEquals('home', $req->getURL());
} }
public function testGetIPFromHeaderValue() public function testSetIPFromHeaderValue()
{ {
$req = new HTTPRequest('GET', '/'); $req = new TrustedProxyMiddleware();
$reflectionMethod = new ReflectionMethod($req, 'getIPFromHeaderValue'); $reflectionMethod = new ReflectionMethod($req, 'getIPFromHeaderValue');
$reflectionMethod->setAccessible(true); $reflectionMethod->setAccessible(true);

View File

@ -109,7 +109,7 @@ class SessionTest extends SapphireTest
{ {
// Set a user agent // Set a user agent
$req1 = new HTTPRequest('GET', '/'); $req1 = new HTTPRequest('GET', '/');
$req1->setHeader('User-Agent', 'Test Agent'); $req1->addHeader('User-Agent', 'Test Agent');
// Generate our session // Generate our session
$s = new Session(array()); $s = new Session(array());
@ -119,7 +119,7 @@ class SessionTest extends SapphireTest
// Change our UA // Change our UA
$req2 = new HTTPRequest('GET', '/'); $req2 = new HTTPRequest('GET', '/');
$req2->setHeader('User-Agent', 'Test Agent'); $req2->addHeader('User-Agent', 'Fake Agent');
// Verify the new session reset our values // Verify the new session reset our values
$s2 = new Session($s); $s2 = new Session($s);

View File

@ -24,6 +24,7 @@ use SilverStripe\Core\Tests\Injector\InjectorTest\TestObject;
use SilverStripe\Core\Tests\Injector\InjectorTest\TestSetterInjections; use SilverStripe\Core\Tests\Injector\InjectorTest\TestSetterInjections;
use SilverStripe\Core\Tests\Injector\InjectorTest\TestStaticInjections; use SilverStripe\Core\Tests\Injector\InjectorTest\TestStaticInjections;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\TestOnly;
use stdClass; use stdClass;
define('TEST_SERVICES', __DIR__ . '/AopProxyServiceTest'); define('TEST_SERVICES', __DIR__ . '/AopProxyServiceTest');
@ -802,10 +803,40 @@ class InjectorTest extends SapphireTest
public function testNamedServices() public function testNamedServices()
{ {
$injector = new Injector(); $injector = new Injector();
$service = new stdClass(); $service = new TestObject();
$service->setSomething('injected');
// Test registering with non-class name
$injector->registerService($service, 'NamedService'); $injector->registerService($service, 'NamedService');
$this->assertTrue($injector->has('NamedService'));
$this->assertEquals($service, $injector->get('NamedService')); $this->assertEquals($service, $injector->get('NamedService'));
// Unregister by name only: New instance of the
// old class will be constructed
$injector->unregisterNamedObject('NamedService');
$this->assertTrue($injector->has('NamedService'));
$this->assertNotEquals($service, $injector->get(TestObject::class));
// Unregister name and spec, injector forgets about this
// service spec altogether
$injector->unregisterNamedObject('NamedService', true);
$this->assertFalse($injector->has('NamedService'));
// Test registered with class name
$injector->registerService($service);
$this->assertTrue($injector->has(TestObject::class));
$this->assertEquals($service, $injector->get(TestObject::class));
// Unregister by name only: New instance of the
// old class will be constructed
$injector->unregisterNamedObject(TestObject::class);
$this->assertTrue($injector->has(TestObject::class));
$this->assertNotEquals($service, $injector->get(TestObject::class));
// Unregister name and spec, injector forgets about this
// service spec altogether
$injector->unregisterNamedObject(TestObject::class, true);
$this->assertFalse($injector->has(TestObject::class));
} }
public function testCreateConfiggedObjectWithCustomConstructorArgs() public function testCreateConfiggedObjectWithCustomConstructorArgs()