Skip to content

Commit

Permalink
API Refactor $flush into HTPPApplication
Browse files Browse the repository at this point in the history
API Enforce health check in Controller::pushCurrent()
API Better global backup / restore
Updated Director::test() to use new API
  • Loading branch information
Damian Mooyman committed Jun 20, 2017
1 parent b220534 commit d1d4375
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 139 deletions.
3 changes: 1 addition & 2 deletions cli-script.php
Expand Up @@ -11,10 +11,9 @@

// Build request and detect flush
$request = HTTPRequest::createFromEnvironment();
$flush = $request->getVar('flush') || strpos($request->getURL(), 'dev/build') === 0;

// Default application
$kernel = new AppKernel($flush);
$kernel = new AppKernel();
$app = new HTTPApplication($kernel);
$app->addMiddleware(new OutputMiddleware());
$app->handle($request);
3 changes: 1 addition & 2 deletions main.php
Expand Up @@ -10,10 +10,9 @@

// Build request and detect flush
$request = HTTPRequest::createFromEnvironment();
$flush = $request->getVar('flush') || strpos($request->getURL(), 'dev/build') === 0;

// Default application
$kernel = new AppKernel($flush);
$kernel = new AppKernel();
$app = new HTTPApplication($kernel);
$app->addMiddleware(new OutputMiddleware());
$app->addMiddleware(new ErrorControlChainMiddleware($app, $request));
Expand Down
9 changes: 7 additions & 2 deletions src/Control/Controller.php
Expand Up @@ -154,10 +154,10 @@ public function setRequest($request)
*/
protected function beforeHandleRequest(HTTPRequest $request)
{
//Push the current controller to protect against weird session issues
$this->pushCurrent();
//Set up the internal dependencies (request, response)
$this->setRequest($request);
//Push the current controller to protect against weird session issues
$this->pushCurrent();
$this->setResponse(new HTTPResponse());
//kick off the init functionality
$this->doInit();
Expand Down Expand Up @@ -588,9 +588,14 @@ public function can($perm, $member = null)
* Pushes this controller onto the stack of current controllers. This means that any redirection,
* session setting, or other things that rely on Controller::curr() will now write to this
* controller object.
*
* Note: Ensure this controller is assigned a request with a valid session before pushing
* it to the stack.
*/
public function pushCurrent()
{
// Ensure this controller has a valid session
$this->getRequest()->getSession();
array_unshift(self::$controller_stack, $this);
}

Expand Down
163 changes: 75 additions & 88 deletions src/Control/Director.php
Expand Up @@ -3,12 +3,10 @@
namespace SilverStripe\Control;

use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\ArrayLib;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Requirements;
use SilverStripe\View\Requirements_Backend;
Expand Down Expand Up @@ -188,130 +186,96 @@ public static function test(
$cookies = array(),
&$request = null
) {
Config::nest();
Injector::nest();
// Build list of cleanup promises
$finally = [];

/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$kernel->nest();
$finally[] = function () use ($kernel) {
$kernel->activate();
};

// backup existing vars, and create new vars
$existingVars = static::envToVars();
$finally[] = function () use ($existingVars) {
static::varsToEnv($existingVars);
};
$newVars = $existingVars;

// These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics.
$oldReadingMode = null;
if (class_exists(Versioned::class)) {
$oldReadingMode = Versioned::get_reading_mode();
$finally[] = function () use ($oldReadingMode) {
Versioned::set_reading_mode($oldReadingMode);
};
}
$getVars = array();

if (!$httpMethod) {
$httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET";
}
// Default httpMethod
$newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod
?: (($postVars || is_array($postVars)) ? "POST" : "GET");

if (!$session) {
$session = new Session([]);
}
// Setup session
$newVars['_SESSION'] = $session instanceof Session
? $session->getAll()
: ($session ?: []);

// Setup cookies
$cookieJar = $cookies instanceof Cookie_Backend
? $cookies
: Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: []));
$newVars['_COOKIE'] = $cookieJar->getAll(false);
Cookie::config()->update('report_errors', false);
Injector::inst()->registerService($cookieJar, Cookie_Backend::class);

// Back up the current values of the superglobals
$existingRequestVars = isset($_REQUEST) ? $_REQUEST : array();
$existingGetVars = isset($_GET) ? $_GET : array();
$existingPostVars = isset($_POST) ? $_POST : array();
$existingSessionVars = isset($_SESSION) ? $_SESSION : array();
$existingCookies = isset($_COOKIE) ? $_COOKIE : array();
$existingServer = isset($_SERVER) ? $_SERVER : array();

// Backup requirements
$existingRequirementsBackend = Requirements::backend();

Cookie::config()->update('report_errors', false);
Requirements::set_backend(Requirements_Backend::create());
$finally[] = function () use ($existingRequirementsBackend) {
Requirements::set_backend($existingRequirementsBackend);
};

if (strpos($url, '#') !== false) {
$url = substr($url, 0, strpos($url, '#'));
}
// Strip any hash
$url = strtok($url, '#');

// Handle absolute URLs
if (parse_url($url, PHP_URL_HOST)) {
$bits = parse_url($url);

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

// Ensure URL is properly made relative.
// Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
$url = self::makeRelative($url);

$urlWithQuerystring = $url;
if (strpos($url, '?') !== false) {
list($url, $getVarsEncoded) = explode('?', $url, 2);
parse_str($getVarsEncoded, $getVars);
parse_str($getVarsEncoded, $newVars['_GET']);
} else {
$newVars['_GET'] = [];
}
$newVars['_SERVER']['REQUEST_URI'] = Director::baseURL() . $url;

// Replace the super globals with appropriate test values
$_REQUEST = ArrayLib::array_merge_recursive((array) $getVars, (array) $postVars);
$_GET = (array) $getVars;
$_POST = (array) $postVars;
$_SESSION = $session ? $session->getAll() : array();
$_COOKIE = $cookieJar->getAll(false);
Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
$_SERVER['REQUEST_URI'] = Director::baseURL() . $urlWithQuerystring;

$request = new HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
// Create new request
$request = HTTPRequest::createFromVariables($newVars, $body);
if ($headers) {
foreach ($headers as $k => $v) {
$request->addHeader($k, $v);
}
}

try {
// Pre-request filtering
$requestProcessor = Injector::inst()->get(RequestProcessor::class);
$output = $requestProcessor->preRequest($request);
if ($output === false) {
throw new HTTPResponse_Exception(_t('SilverStripe\\Control\\Director.INVALID_REQUEST', 'Invalid request'), 400);
}

// Process request
$result = Director::handleRequest($request);

// Ensure that the result is an HTTPResponse object
if (is_string($result)) {
if (substr($result, 0, 9) == 'redirect:') {
$response = new HTTPResponse();
$response->redirect(substr($result, 9));
$result = $response;
} else {
$result = new HTTPResponse($result);
}
}

$output = $requestProcessor->postRequest($request, $result);
if ($output === false) {
throw new HTTPResponse_Exception("Invalid response");
}

// Return valid response
return $result;
// Normal request handling
return static::direct($request);
} finally {
// Restore the super globals
$_REQUEST = $existingRequestVars;
$_GET = $existingGetVars;
$_POST = $existingPostVars;
$_SESSION = $existingSessionVars;
$_COOKIE = $existingCookies;
$_SERVER = $existingServer;

Requirements::set_backend($existingRequirementsBackend);

// These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics
if (class_exists(Versioned::class)) {
Versioned::set_reading_mode($oldReadingMode);
// Restore state in reverse order to assignment
foreach (array_reverse($finally) as $callback) {
call_user_func($callback);
}

Injector::unnest(); // Restore old CookieJar, etc
Config::unnest();
}
}

Expand Down Expand Up @@ -372,6 +336,29 @@ protected static function handleRequest(HTTPRequest $request)
return new HTTPResponse('No URL rule was matched', 404);
}

/**
* Extract env vars prior to modification
*
* @return array List of all super globals
*/
public static function envToVars()
{
// Suppress return by-ref
return array_merge($GLOBALS, []);
}

/**
* Restore a backed up or modified list of vars to $globals
*
* @param array $vars
*/
public static function varsToEnv(array $vars)
{
foreach ($vars as $key => $value) {
$GLOBALS[$key] = $value;
}
}

/**
* 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.
Expand Down
18 changes: 4 additions & 14 deletions src/Control/HTTPRequest.php
Expand Up @@ -2,10 +2,10 @@

namespace SilverStripe\Control;

use ArrayAccess;
use BadMethodCallException;
use SilverStripe\Core\ClassInfo;
use SilverStripe\ORM\ArrayLib;
use ArrayAccess;

/**
* Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method
Expand Down Expand Up @@ -159,8 +159,8 @@ public function __construct($httpMethod, $url, $getVars = array(), $postVars = a
public static function createFromEnvironment()
{
// Health-check prior to creating environment
$variables = static::variablesFromEnvironment();
return self::createFromVariables($variables, @file_get_contents('php://input'));
static::validateEnvironment();
return self::createFromVariables(Director::envToVars(), @file_get_contents('php://input'));
}

/**
Expand Down Expand Up @@ -256,9 +256,8 @@ public static function cleanEnvironment(array $variables)
* Error conditions will raise HTTPResponse_Exceptions
*
* @throws HTTPResponse_Exception
* @return array
*/
protected static function variablesFromEnvironment()
protected static function validateEnvironment()
{
// Validate $_FILES array before merging it with $_POST
foreach ($_FILES as $key => $value) {
Expand All @@ -284,15 +283,6 @@ protected static function variablesFromEnvironment()
throw new HTTPResponse_Exception('Invalid Host', 400);
}
}

return [
'_SERVER' => $_SERVER,
'_GET' => $_GET,
'_POST' => $_POST,
'_FILES' => $_FILES,
'_SESSION' => isset($_SESSION) ? $_SESSION : null,
'_COOKIE' => $_COOKIE
];
}

/**
Expand Down
28 changes: 10 additions & 18 deletions src/Core/AppKernel.php
Expand Up @@ -28,15 +28,8 @@

class AppKernel extends CoreKernel
{
/**
* @var bool
*/
protected $flush = false;

public function __construct($flush = false)
public function __construct()
{
$this->flush = $flush;

// Initialise the dependency injector as soon as possible, as it is
// subsequently used by some of the following code
$injectorLoader = InjectorLoader::inst();
Expand Down Expand Up @@ -134,13 +127,10 @@ protected function sessionEnvironment()
return null;
}

/**
* @throws HTTPResponse_Exception
*/
public function boot()
public function boot($flush = false)
{
$this->bootPHP();
$this->bootManifests();
$this->bootManifests($flush);
$this->bootErrorHandling();
$this->bootDatabase();
}
Expand Down Expand Up @@ -374,17 +364,19 @@ protected function getIncludeTests()

/**
* Boot all manifests
*
* @param bool $flush
*/
protected function bootManifests()
protected function bootManifests($flush)
{
// Setup autoloader
$this->getClassLoader()->init($this->getIncludeTests(), $this->flush);
$this->getClassLoader()->init($this->getIncludeTests(), $flush);

// Find modules
$this->getModuleLoader()->init($this->getIncludeTests(), $this->flush);
$this->getModuleLoader()->init($this->getIncludeTests(), $flush);

// Flush config
if ($this->flush) {
if ($flush) {
$config = $this->getConfigLoader()->getManifest();
if ($config instanceof CachedConfigCollection) {
$config->setFlush(true);
Expand All @@ -397,7 +389,7 @@ protected function bootManifests()
// Find default templates
$defaultSet = $this->getThemeResourceLoader()->getSet('$default');
if ($defaultSet instanceof ThemeManifest) {
$defaultSet->init($this->getIncludeTests(), $this->flush);
$defaultSet->init($this->getIncludeTests(), $flush);
}
}

Expand Down

0 comments on commit d1d4375

Please sign in to comment.