Skip to content

Commit

Permalink
BUG Fix forceWWW and forceSSL not working in _config.php
Browse files Browse the repository at this point in the history
Fixes #7492
  • Loading branch information
Damian Mooyman committed Oct 26, 2017
1 parent c020061 commit e06157d
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 30 deletions.
97 changes: 73 additions & 24 deletions src/Control/Director.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
use SilverStripe\Control\Middleware\RegisteredEventsMiddleware;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\CoreKernel;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
Expand Down Expand Up @@ -85,6 +87,15 @@ class Director implements TemplateGlobalProvider
*/
private static $alternate_base_url;

/**
* Environments that forceWWW() works in
*
* @var array
*/
private static $force_www_envs = [
CoreKernel::LIVE,
];

/**
* Base url to populate if cannot be determined otherwise.
* Supports back-ticked vars; E.g. '`SS_BASE_URL`'
Expand Down Expand Up @@ -242,7 +253,7 @@ public static function mockRequest(

// If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
$newVars['_SERVER']['HTTP_HOST'] = isset($bits['port'])
? $bits['host'].':'.$bits['port']
? $bits['host'] . ':' . $bits['port']
: $bits['host'];
}

Expand Down Expand Up @@ -697,10 +708,10 @@ public static function is_absolute_url($url)
{
// Strip off the query and fragment parts of the URL before checking
if (($queryPosition = strpos($url, '?')) !== false) {
$url = substr($url, 0, $queryPosition-1);
$url = substr($url, 0, $queryPosition - 1);
}
if (($hashPosition = strpos($url, '#')) !== false) {
$url = substr($url, 0, $hashPosition-1);
$url = substr($url, 0, $hashPosition - 1);
}
$colonPosition = strpos($url, ':');
$slashPosition = strpos($url, '/');
Expand Down Expand Up @@ -809,7 +820,7 @@ public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
$login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@";
}

return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
}

/**
Expand Down Expand Up @@ -855,26 +866,29 @@ protected static function force_redirect($destURL)
*
* @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
* @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
* @return bool true if already on SSL, false if doesn't match patterns (or cannot redirect)
* @throws HTTPResponse_Exception Throws exception with redirect, if successful
* @param HTTPRequest|null $request Request object to check
* @return bool true if already on SSL, false if doesn't match patterns (or cannot redirect),
* or null if could not immediately redirect.
*/
public static function forceSSL($patterns = null, $secureDomain = null)
public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
{
// If request isn't available register promise
if (!$request) {
return static::whenRequestIsAvailable(function (HTTPRequest $request) use ($patterns, $secureDomain) {
return self::forceSSL($patterns, $secureDomain, $request);
}, __METHOD__);
}

// Already on SSL
if (static::is_https()) {
if (static::is_https($request)) {
return true;
}

// Can't redirect without a url
if (!isset($_SERVER['REQUEST_URI'])) {
return false;
}
$relativeURL = $request->getURL(true);

// protect portions of the site based on the pattern
if ($patterns) {
$matched = false;
$relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));

// protect portions of the site based on the pattern
foreach ($patterns as $pattern) {
if (preg_match($pattern, $relativeURL)) {
$matched = true;
Expand All @@ -888,9 +902,9 @@ public static function forceSSL($patterns = null, $secureDomain = null)

// if an domain is specified, redirect to that instead of the current domain
if (!$secureDomain) {
$secureDomain = static::host();
$secureDomain = static::host($request);
}
$url = 'https://' . $secureDomain . $_SERVER['REQUEST_URI'];
$url = Controller::join_links("https://{$secureDomain}", self::baseURL(), $relativeURL);

// Force redirect
self::force_redirect($url);
Expand All @@ -899,16 +913,30 @@ public static function forceSSL($patterns = null, $secureDomain = null)

/**
* Force a redirect to a domain starting with "www."
*
* @param HTTPRequest $request
*/
public static function forceWWW()
public static function forceWWW(HTTPRequest $request = null)
{
if (!Director::isDev() && !Director::isTest() && strpos(static::host(), 'www') !== 0) {
$destURL = str_replace(
Director::protocol(),
Director::protocol() . 'www.',
Director::absoluteURL($_SERVER['REQUEST_URI'])
);
// Skip if not in allowed envs
$allowed = static::config()->get('force_www_envs');
if ($allowed && !in_array(static::get_environment_type(), $allowed)) {
return;
}

// If request isn't available register promise
if (!$request) {
static::whenRequestIsAvailable(function (HTTPRequest $request) {
static::forceWWW($request);
}, __METHOD__);
return;
}

$host = static::host($request);
if (strpos($host, 'www') !== 0) {
$protocol = self::protocol($request);
$url = self::absoluteURL($request->getURL(true));
$destURL = str_replace($protocol, $protocol . 'www.', $url);
self::force_redirect($destURL);
}
}
Expand Down Expand Up @@ -1021,4 +1049,25 @@ protected static function currentRequest(HTTPRequest $request = null)
}
return $request;
}

/**
* Promise to invoke the given callback once the request is available
*
* @param callable $callback
* @param string $event Optional event name
* @return mixed Result of $callback, or null if could not be immediately called
*/
public static function whenRequestIsAvailable($callback, $event = null)
{
// Can immediately resolve
$request = static::currentRequest();
if ($request) {
return $callback($request);
}

// Resolve later
$handler = RegisteredEventsMiddleware::singleton();
$handler->registerEvent($callback, $event);
return null;
}
}
68 changes: 62 additions & 6 deletions tests/php/Control/DirectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Control\Middleware\RegisteredEventsMiddleware;
use SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter;
use SilverStripe\Control\Middleware\TrustedProxyMiddleware;
use SilverStripe\Control\RequestProcessor;
Expand Down Expand Up @@ -412,19 +412,71 @@ public function testRouteParams()
);
}

public function testForceWWW()
{
Director::config()
->set('alternate_base_url', 'http://mysite.com/')
->set('force_www_envs', []);

$this->expectExceptionRedirect('http://www.mysite.com/some-url');
Director::mockRequest(function ($request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceWWW();
}, '/some-url');
}

public function testPromisedForceWWW()
{
Director::config()
->set('alternate_base_url', 'http://mysite.com/')
->set('force_www_envs', []);
Director::forceWWW();

// Promise is registered but not resolved
$middleware = RegisteredEventsMiddleware::singleton();
$this->assertTrue($middleware->hasEvent(Director::class.'::forceWWW'));

// Middleware forces the redirection eventually
$this->expectExceptionRedirect('http://www.mysite.com/some-url');
Director::mockRequest(function ($request) use ($middleware) {
$middleware->process($request, function ($request) {
return $request;
});
}, '/some-url');
}

public function testForceSSLProtectsEntireSite()
{
$this->expectExceptionRedirect('https://www.mysite.com/some-url');
Director::mockRequest(function () {
Director::mockRequest(function ($request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceSSL();
}, '/some-url');
}

public function testPromisedForceSSL()
{
Director::forceSSL();

// Promise is registered but not resolved
$middleware = RegisteredEventsMiddleware::singleton();
$this->assertTrue($middleware->hasEvent(Director::class.'::forceSSL'));

// Middleware forces the redirection eventually
$this->expectExceptionRedirect('https://www.mysite.com/some-url');
Director::mockRequest(function ($request) use ($middleware) {
$middleware->process($request, function ($request) {
return $request;
});
}, '/some-url');
}

public function testForceSSLOnTopLevelPagePattern()
{
// Expect admin to trigger redirect
$this->expectExceptionRedirect('https://www.mysite.com/admin');
Director::mockRequest(function () {
Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceSSL(array('/^admin/'));
}, '/admin');
}
Expand All @@ -433,20 +485,23 @@ public function testForceSSLOnSubPagesPattern()
{
// Expect to redirect to security login page
$this->expectExceptionRedirect('https://www.mysite.com/Security/login');
Director::mockRequest(function () {
Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
Director::forceSSL(array('/^Security/'));
}, '/Security/login');
}

public function testForceSSLWithPatternDoesNotMatchOtherPages()
{
// Not on same url should not trigger redirect
Director::mockRequest(function () {
Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
$this->assertFalse(Director::forceSSL(array('/^admin/')));
}, Director::baseURL() . 'normal-page');

// nested url should not triger redirect either
Director::mockRequest(function () {
Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
$this->assertFalse(Director::forceSSL(array('/^admin/', '/^Security/')));
}, Director::baseURL() . 'just-another-page/sub-url');
}
Expand All @@ -456,6 +511,7 @@ public function testForceSSLAlternateDomain()
// Ensure that forceSSL throws the appropriate exception
$this->expectExceptionRedirect('https://secure.mysite.com/admin');
Director::mockRequest(function (HTTPRequest $request) {
Injector::inst()->registerService($request, HTTPRequest::class);
return Director::forceSSL(array('/^admin/'), 'secure.mysite.com');
}, Director::baseURL() . 'admin');
}
Expand Down

0 comments on commit e06157d

Please sign in to comment.