diff --git a/cli-script.php b/cli-script.php index f6515247e..5d3cf5cca 100755 --- a/cli-script.php +++ b/cli-script.php @@ -1,8 +1,7 @@ General and Core Removed API @@ -1523,6 +1524,7 @@ The below methods have been added or had their functionality updated to `DBDate` #### ORM Removed API +* `DataModel` removed * `DataObject::can*` methods no longer accept a member ID. These must now be passed a Member object or left null * `DataObject::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs` * `DataObject::manyManyComponent` moved to `DataObjectSchema` diff --git a/main.php b/main.php index 8bcd53be6..3912a107d 100644 --- a/main.php +++ b/main.php @@ -1,226 +1,26 @@ url=[^&?]*)(?.*[&?]url=.*)$/', $_SERVER['QUERY_STRING'], $results) - ) { - $queryString = $results['query'].'&'.$results['url']; - $parseQuery($queryString); - } - - $url = $_GET['url']; - - // IIS includes get variables in url - $i = strpos($url, '?'); - if($i !== false) { - $url = substr($url, 0, $i); - } - - // Lighttpd and PHP 5.4's built-in webserver use this -} else { - // Get raw URL -- still needs to be decoded below (after parsing out query string). - $url = $_SERVER['REQUEST_URI']; - - // Querystring args need to be explicitly parsed - if(strpos($url,'?') !== false) { - list($url, $query) = explode('?',$url,2); - $parseQuery($query); - } - - // Decode URL now that it has been separated from query string. - $url = urldecode($url); - - // Pass back to the webserver for files that exist - if(php_sapi_name() === 'cli-server' && file_exists(BASE_PATH . $url) && is_file(BASE_PATH . $url)) { - return false; - } -} - -// Remove base folders from the URL if webroot is hosted in a subfolder -if (substr(strtolower($url), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) $url = substr($url, strlen(BASE_URL)); - -/** - * Include SilverStripe's core code - */ -require_once('Core/Startup/ErrorControlChain.php'); -require_once('Core/Startup/ParameterConfirmationToken.php'); - -// Prepare tokens and execute chain -$reloadToken = ParameterConfirmationToken::prepare_tokens(array('isTest', 'isDev', 'flush')); -$chain = new ErrorControlChain(); -$chain - ->then(function($chain) use ($reloadToken) { - // If no redirection is necessary then we can disable error supression - if (!$reloadToken) $chain->setSuppression(false); - - // Load in core - require_once('Core/Core.php'); - - // Connect to database - global $databaseConfig; - if ($databaseConfig) DB::connect($databaseConfig); - - // Check if a token is requesting a redirect - if (!$reloadToken) return; - - // Otherwise, we start up the session if needed - if(!isset($_SESSION) && Session::request_contains_session_id()) { - Session::start(); - } - - // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin - if (Director::isDev() || !Security::database_is_ready() || Permission::check('ADMIN')) { - $reloadToken->reloadWithToken(); - return; - } - - // Fail and redirect the user to the login page - $loginPage = Director::absoluteURL(Security::config()->login_url); - $loginPage .= "?BackURL=" . urlencode($_SERVER['REQUEST_URI']); - header('location: '.$loginPage, true, 302); - die; - }) - // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function() use ($reloadToken){ - if ($reloadToken) { - $reloadToken->reloadWithToken(); - } - }) - ->execute(); - -global $databaseConfig; - -// Redirect to the installer if no database is selected -if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseConfig['database']) { - - // Is there an _ss_environment.php file? - if(file_exists(BASE_PATH . '/_ss_environment.php') || file_exists(dirname(BASE_PATH) . '/_ss_environment.php')) { - header($_SERVER['SERVER_PROTOCOL'] . " 500 Server Error"); - $dv = new SilverStripe\Dev\DebugView(); - echo $dv->renderHeader(); - echo $dv->renderInfo( - "Configuraton Error", - Director::absoluteBaseURL() - ); - echo $dv->renderParagraph( - 'You need to replace your _ss_environment.php file with a .env file, or with environment variables.

' - . 'See the ' - . 'Environment Management docs for more information.' - ); - echo $dv->renderFooter(); - - die(); - } - - if(!file_exists(BASE_PATH . '/install.php')) { - header($_SERVER['SERVER_PROTOCOL'] . " 500 Server Error"); - die('SilverStripe Framework requires a $databaseConfig defined.'); - } - $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; - $s = (isset($_SERVER['SSL']) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) ? 's' : ''; - $installURL = "http$s://" . $host . BASE_URL . '/install.php'; - - // The above dirname() will equate to "\" on Windows when installing directly from http://localhost (not using - // a sub-directory), this really messes things up in some browsers. Let's get rid of the backslashes - $installURL = str_replace('\\', '', $installURL); - - header("Location: $installURL"); - die(); -} - -// Direct away - this is the "main" function, that hands control to the appropriate controller -DataModel::set_inst(new DataModel()); -Director::direct($url, DataModel::inst()); +// Default application +$request = HTTPRequest::createFromEnvironment(); +$kernel = new AppKernel(); +$app = new HTTPApplication($kernel); +$app->addMiddleware(new OutputMiddleware()); +$app->addMiddleware(new ErrorControlChainMiddleware($app, $request)); +$app->handle($request); diff --git a/main.php5 b/main.php5 deleted file mode 100644 index f89e90eee..000000000 --- a/main.php5 +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/src/Control/Controller.php b/src/Control/Controller.php index 9f2e6f9b3..c0d088e5d 100644 --- a/src/Control/Controller.php +++ b/src/Control/Controller.php @@ -3,9 +3,7 @@ namespace SilverStripe\Control; use SilverStripe\Core\ClassInfo; -use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\Debug; -use SilverStripe\ORM\DataModel; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\BasicAuth; use SilverStripe\Security\Member; @@ -47,13 +45,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider */ protected $action; - /** - * The {@link Session} object for this controller. - * - * @var Session - */ - protected $session; - /** * Stack of current controllers. Controller::$controller_stack[0] is the current controller. * @@ -152,16 +143,14 @@ class Controller extends RequestHandler implements TemplateGlobalProvider * @todo setDataModel and setRequest are redundantly called in parent::handleRequest() - sort this out * * @param HTTPRequest $request - * @param DataModel $model */ - protected function beforeHandleRequest(HTTPRequest $request, DataModel $model) + protected function beforeHandleRequest(HTTPRequest $request) { //Push the current controller to protect against weird session issues $this->pushCurrent(); - //Set up the internal dependencies (request, response, datamodel) + //Set up the internal dependencies (request, response) $this->setRequest($request); $this->setResponse(new HTTPResponse()); - $this->setDataModel($model); //kick off the init functionality $this->doInit(); } @@ -192,24 +181,22 @@ class Controller extends RequestHandler implements TemplateGlobalProvider * and end the method with $this->afterHandleRequest() * * @param HTTPRequest $request - * @param DataModel $model - * * @return HTTPResponse */ - public function handleRequest(HTTPRequest $request, DataModel $model) + public function handleRequest(HTTPRequest $request) { if (!$request) { user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR); } //set up the controller for the incoming request - $this->beforeHandleRequest($request, $model); + $this->beforeHandleRequest($request); //if the before handler manipulated the response in a way that we shouldn't proceed, then skip our request // handling if (!$this->getResponse()->isFinished()) { //retrieve the response for the request - $response = parent::handleRequest($request, $model); + $response = parent::handleRequest($request); //prepare the response (we can receive an assortment of response types (strings/objects/HTTPResponses) $this->prepareResponse($response); @@ -597,14 +584,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider public function pushCurrent() { array_unshift(self::$controller_stack, $this); - // Create a new session object - if (!$this->session) { - if (isset(self::$controller_stack[1])) { - $this->session = self::$controller_stack[1]->getSession(); - } else { - $this->session = Injector::inst()->create('SilverStripe\\Control\\Session', array()); - } - } } /** @@ -653,26 +632,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider return $this->getResponse() && $this->getResponse()->getHeader('Location'); } - /** - * Get the Session object representing this Controller's session. - * - * @return Session - */ - public function getSession() - { - return $this->session; - } - - /** - * Set the Session object. - * - * @param Session $session - */ - public function setSession(Session $session) - { - $this->session = $session; - } - /** * Joins two or more link segments together, putting a slash between them if necessary. Use this * for building the results of {@link Link()} methods. If either of the links have query strings, diff --git a/src/Control/Director.php b/src/Control/Director.php index 19aa27e7b..ee9a8d9a1 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -2,15 +2,14 @@ namespace SilverStripe\Control; -use InvalidArgumentException; 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\Dev\SapphireTest; use SilverStripe\ORM\ArrayLib; -use SilverStripe\ORM\DataModel; use SilverStripe\Versioned\Versioned; use SilverStripe\View\Requirements; use SilverStripe\View\Requirements_Backend; @@ -120,112 +119,41 @@ class Director implements TemplateGlobalProvider * * @uses handleRequest() rule-lookup logic is handled by this. * @uses TestController::handleRequest() This handles the page logic for a Director::direct() call. - * @param string $url - * @param DataModel $model + * @param HTTPRequest $request + * @return HTTPResponse * @throws HTTPResponse_Exception */ - public static function direct($url, DataModel $model) + public static function direct(HTTPRequest $request) { - // check allowed hosts - if (getenv('SS_ALLOWED_HOSTS') && !Director::is_cli()) { - $all_allowed_hosts = explode(',', getenv('SS_ALLOWED_HOSTS')); - if (!in_array(static::host(), $all_allowed_hosts)) { - throw new HTTPResponse_Exception('Invalid Host', 400); - } - } - - - // Validate $_FILES array before merging it with $_POST - foreach ($_FILES as $k => $v) { - if (is_array($v['tmp_name'])) { - $v = ArrayLib::array_values_recursive($v['tmp_name']); - foreach ($v as $tmpFile) { - if ($tmpFile && !is_uploaded_file($tmpFile)) { - user_error("File upload '$k' doesn't appear to be a valid upload", E_USER_ERROR); - } - } - } else { - if ($v['tmp_name'] && !is_uploaded_file($v['tmp_name'])) { - user_error("File upload '$k' doesn't appear to be a valid upload", E_USER_ERROR); - } - } - } - - $req = new HTTPRequest( - (isset($_SERVER['X-HTTP-Method-Override'])) - ? $_SERVER['X-HTTP-Method-Override'] - : $_SERVER['REQUEST_METHOD'], - $url, - $_GET, - ArrayLib::array_merge_recursive((array) $_POST, (array) $_FILES), - @file_get_contents('php://input') - ); - - $headers = self::extract_request_headers($_SERVER); - foreach ($headers as $header => $value) { - $req->addHeader($header, $value); - } - - // Initiate an empty session - doesn't initialize an actual PHP session until saved (see below) - $session = Session::create(isset($_SESSION) ? $_SESSION : array()); - - // Only resume a session if its not started already, and a session identifier exists - if (!isset($_SESSION) && Session::request_contains_session_id()) { - $session->inst_start(); - } - - $output = RequestProcessor::singleton()->preRequest($req, $session, $model); - + // Pre-request + $output = RequestProcessor::singleton()->preRequest($request); if ($output === false) { - // @TODO Need to NOT proceed with the request in an elegant manner - throw new HTTPResponse_Exception(_t('SilverStripe\\Control\\Director.INVALID_REQUEST', 'Invalid request'), 400); + return new HTTPResponse(_t(__CLASS__.'.INVALID_REQUEST', 'Invalid request'), 400); } - $result = Director::handleRequest($req, $session, $model); + // Generate output + $result = static::handleRequest($request); // Save session data. Note that inst_save() will start/resume the session if required. - $session->inst_save(); + $request->getSession()->save(); // Return code for a redirection request - if (is_string($result) && substr($result, 0, 9) == 'redirect:') { - $url = substr($result, 9); - - if (Director::is_cli()) { - // on cli, follow SilverStripe redirects automatically - Director::direct( - str_replace(Director::absoluteBaseURL(), '', $url), - DataModel::inst() - ); - return; - } else { - $response = new HTTPResponse(); - $response->redirect($url); - $res = RequestProcessor::singleton()->postRequest($req, $response, $model); - - if ($res !== false) { - $response->output(); - } - } - // Handle a controller - } elseif ($result) { - if ($result instanceof HTTPResponse) { - $response = $result; - } else { - $response = new HTTPResponse(); - $response->setBody($result); - } - - $res = RequestProcessor::singleton()->postRequest($req, $response, $model); - if ($res !== false) { - $response->output(); - } else { - // @TODO Proper response here. - throw new HTTPResponse_Exception("Invalid response"); - } - - - //$controllerObj->getSession()->inst_save(); + // @todo: Refactor into CLIApplication + if ($result->isRedirect() && static::is_cli()) { + $url = Director::makeRelative($result->getHeader('Location')); + $request = clone $request; + $request->setUrl($url); + return static::direct($request); } + + // Post-request handling + $postRequest = RequestProcessor::singleton()->postRequest($request, $result); + if ($postRequest === false) { + return new HTTPResponse(_t(__CLASS__ . '.REQUEST_ABORTED', 'Request aborted'), 500); + } + + // Return + return $result; } /** @@ -278,7 +206,7 @@ class Director implements TemplateGlobalProvider } if (!$session) { - $session = Session::create([]); + $session = new Session([]); } $cookieJar = $cookies instanceof Cookie_Backend ? $cookies @@ -340,15 +268,14 @@ class Director implements TemplateGlobalProvider try { // Pre-request filtering - $model = DataModel::inst(); $requestProcessor = Injector::inst()->get(RequestProcessor::class); - $output = $requestProcessor->preRequest($request, $session, $model); + $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, $session, $model); + $result = Director::handleRequest($request); // Ensure that the result is an HTTPResponse object if (is_string($result)) { @@ -395,15 +322,14 @@ class Director implements TemplateGlobalProvider * * @skipUpgrade * @param HTTPRequest $request - * @param Session $session - * @param DataModel $model - * @return HTTPResponse|string + * @return HTTPResponse */ - protected static function handleRequest(HTTPRequest $request, Session $session, DataModel $model) + protected static function handleRequest(HTTPRequest $request) { $rules = Director::config()->uninherited('rules'); foreach ($rules as $pattern => $controllerOptions) { + // Normalise route rule if (is_string($controllerOptions)) { if (substr($controllerOptions, 0, 2) == '->') { $controllerOptions = array('Redirect' => substr($controllerOptions, 2)); @@ -412,7 +338,9 @@ class Director implements TemplateGlobalProvider } } - if (($arguments = $request->match($pattern, true)) !== false) { + // Match pattern + $arguments = $request->match($pattern, true); + if ($arguments !== false) { $request->setRouteParams($controllerOptions); // controllerOptions provide some default arguments $arguments = array_merge($controllerOptions, $arguments); @@ -424,24 +352,20 @@ class Director implements TemplateGlobalProvider // Handle redirection if (isset($arguments['Redirect'])) { - return "redirect:" . Director::absoluteURL($arguments['Redirect'], true); - } else { - // Find the controller name - $controller = $arguments['Controller']; - $controllerObj = Injector::inst()->create($controller); - $controllerObj->setSession($session); + // Redirection + $response = new HTTPResponse(); + $response->redirect(static::absoluteURL($arguments['Redirect'])); + return $response; + } - try { - $result = $controllerObj->handleRequest($request, $model); - } catch (HTTPResponse_Exception $responseException) { - $result = $responseException->getResponse(); - } - if (!is_object($result) || $result instanceof HTTPResponse) { - return $result; - } + // Find the controller name + $controller = $arguments['Controller']; + $controllerObj = Injector::inst()->create($controller); - user_error("Bad result from url " . $request->getURL() . " handled by " . - get_class($controllerObj)." controller: ".get_class($result), E_USER_WARNING); + try { + return $controllerObj->handleRequest($request); + } catch (HTTPResponse_Exception $responseException) { + return $responseException->getResponse(); } } } @@ -852,36 +776,6 @@ class Director implements TemplateGlobalProvider } } - /** - * Takes a $_SERVER data array and extracts HTTP request headers. - * - * @param array $server - * - * @return array - */ - public static function extract_request_headers(array $server) - { - $headers = array(); - - foreach ($server as $key => $value) { - if (substr($key, 0, 5) == 'HTTP_') { - $key = substr($key, 5); - $key = strtolower(str_replace('_', ' ', $key)); - $key = str_replace(' ', '-', ucwords($key)); - $headers[$key] = $value; - } - } - - if (isset($server['CONTENT_TYPE'])) { - $headers['Content-Type'] = $server['CONTENT_TYPE']; - } - if (isset($server['CONTENT_LENGTH'])) { - $headers['Content-Length'] = $server['CONTENT_LENGTH']; - } - - return $headers; - } - /** * Given a filesystem reference relative to the site root, return the full file-system path. * @@ -1073,47 +967,7 @@ class Director implements TemplateGlobalProvider */ public static function is_cli() { - return (php_sapi_name() == "cli"); - } - - /** - * Set the environment type of the current site. - * - * Typically, a SilverStripe site have a number of environments: - * - Development environments, such a copy on your local machine. - * - Test sites, such as the one you show the client before going live. - * - The live site itself. - * - * The behaviour of these environments often varies slightly. For example, development sites may - * have errors dumped to the screen, and order confirmation emails might be sent to the developer - * instead of the client. - * - * To help with this, SilverStripe supports the notion of an environment type. The environment - * type can be dev, test, or live. - * - * Dev mode can also be forced by putting ?isDev=1 in your URL, which will ask you to log in and - * then push the site into dev mode for the remainder of the session. Putting ?isDev=0 onto the URL - * can turn it back. - * - * Test mode can also be forced by putting ?isTest=1 in your URL, which will ask you to log in and - * then push the site into test mode for the remainder of the session. Putting ?isTest=0 onto the URL - * can turn it back. - * - * Generally speaking, these methods will be called from your _config.php file. - * - * Once the environment type is set, it can be checked with {@link Director::isDev()}, - * {@link Director::isTest()}, and {@link Director::isLive()}. - * - * @param string $environment - */ - public static function set_environment_type($environment) - { - if (!in_array($environment, ['dev', 'test', 'live'])) { - throw new InvalidArgumentException( - "Director::set_environment_type passed '$environment'. It should be passed dev, test, or live" - ); - } - self::$environment_type = $environment; + return php_sapi_name() === "cli"; } /** @@ -1124,22 +978,9 @@ class Director implements TemplateGlobalProvider */ public static function get_environment_type() { - // Check saved session - if ($env = self::session_environment()) { - return $env; - } - - // Check set - if (self::$environment_type) { - return self::$environment_type; - } - - // Check getenv - if ($env = getenv('SS_ENVIRONMENT_TYPE')) { - return $env; - } - - return 'live'; + /** @var Kernel $kernel */ + $kernel = Injector::inst()->get(Kernel::class); + return $kernel->getEnvironment(); } /** @@ -1175,37 +1016,6 @@ class Director implements TemplateGlobalProvider return self::get_environment_type() === 'test'; } - /** - * Check or update any temporary environment specified in the session. - * - * @return null|string - */ - public static function session_environment() - { - // Set session from querystring - if (isset($_GET['isDev'])) { - if (isset($_SESSION)) { - unset($_SESSION['isTest']); // In case we are changing from test mode - $_SESSION['isDev'] = $_GET['isDev']; - } - return 'dev'; - } elseif (isset($_GET['isTest'])) { - if (isset($_SESSION)) { - unset($_SESSION['isDev']); // In case we are changing from dev mode - $_SESSION['isTest'] = $_GET['isTest']; - } - return 'test'; - } - // Check session - if (isset($_SESSION['isDev']) && $_SESSION['isDev']) { - return 'dev'; - } elseif (isset($_SESSION['isTest']) && $_SESSION['isTest']) { - return 'test'; - } else { - return null; - } - } - /** * Returns an array of strings of the method names of methods on the call that should be exposed * as global variables in the templates. diff --git a/src/Control/Email/Email.php b/src/Control/Email/Email.php index a01bf6f0d..627888e4c 100644 --- a/src/Control/Email/Email.php +++ b/src/Control/Email/Email.php @@ -96,6 +96,72 @@ class Email extends ViewableData return \Swift_Validate::email($address); } + /** + * Get send_all_emails_to + * + * @return array Keys are addresses, values are names + */ + public static function getSendAllEmailsTo() + { + return static::mergeConfiguredEmails('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO'); + } + + /** + * Get cc_all_emails_to + * + * @return array + */ + public static function getCCAllEmailsTo() + { + return static::mergeConfiguredEmails('cc_all_emails_to', 'SS_CC_ALL_EMAILS_TO'); + } + + /** + * Get bcc_all_emails_to + * + * @return array + */ + public static function getBCCAllEmailsTo() + { + return static::mergeConfiguredEmails('bcc_all_emails_to', 'SS_BCC_ALL_EMAILS_TO'); + } + + /** + * Get send_all_emails_from + * + * @return array + */ + public static function getSendAllEmailsFrom() + { + return static::mergeConfiguredEmails('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM'); + } + + /** + * Normalise email list from config merged with env vars + * + * @param string $config Config key + * @param string $env Env variable key + * @return array Array of email addresses + */ + protected static function mergeConfiguredEmails($config, $env) + { + // Normalise config list + $normalised = []; + $source = (array)static::config()->get($config); + foreach ($source as $address => $name) { + if ($address && !is_numeric($address)) { + $normalised[$address] = $name; + } elseif ($name) { + $normalised[$name] = null; + } + } + $extra = getenv($env); + if ($extra) { + $normalised[$extra] = null; + } + return $normalised; + } + /** * Encode an email-address to protect it from spambots. * At the moment only simple string substitutions, diff --git a/src/Control/Email/SwiftPlugin.php b/src/Control/Email/SwiftPlugin.php index b00edc09e..a0274731f 100644 --- a/src/Control/Email/SwiftPlugin.php +++ b/src/Control/Email/SwiftPlugin.php @@ -11,36 +11,29 @@ class SwiftPlugin implements \Swift_Events_SendListener */ public function beforeSendPerformed(\Swift_Events_SendEvent $evt) { - /** @var \Swift_Message $message */ $message = $evt->getMessage(); - $sendAllTo = Email::config()->send_all_emails_to; - $ccAllTo = Email::config()->cc_all_emails_to; - $bccAllTo = Email::config()->bcc_all_emails_to; - $sendAllFrom = Email::config()->send_all_emails_from; + $sendAllTo = Email::getSendAllEmailsTo(); if (!empty($sendAllTo)) { $this->setTo($message, $sendAllTo); } + $ccAllTo = Email::getCCAllEmailsTo(); if (!empty($ccAllTo)) { - if (!is_array($ccAllTo)) { - $ccAllTo = array($ccAllTo => null); - } foreach ($ccAllTo as $address => $name) { $message->addCc($address, $name); } } + $bccAllTo = Email::getBCCAllEmailsTo(); if (!empty($bccAllTo)) { - if (!is_array($bccAllTo)) { - $bccAllTo = array($bccAllTo => null); - } foreach ($bccAllTo as $address => $name) { $message->addBcc($address, $name); } } + $sendAllFrom = Email::getSendAllEmailsFrom(); if (!empty($sendAllFrom)) { $this->setFrom($message, $sendAllFrom); } @@ -48,7 +41,7 @@ class SwiftPlugin implements \Swift_Events_SendListener /** * @param \Swift_Mime_Message $message - * @param string $to + * @param array|string $to */ protected function setTo($message, $to) { @@ -70,7 +63,7 @@ class SwiftPlugin implements \Swift_Events_SendListener /** * @param \Swift_Mime_Message $message - * @param string $from + * @param array|string $from */ protected function setFrom($message, $from) { diff --git a/src/Control/FlushRequestFilter.php b/src/Control/FlushRequestFilter.php index 594f5aaac..2f7c0a694 100644 --- a/src/Control/FlushRequestFilter.php +++ b/src/Control/FlushRequestFilter.php @@ -3,7 +3,6 @@ namespace SilverStripe\Control; use SilverStripe\Core\Flushable; -use SilverStripe\ORM\DataModel; use SilverStripe\Core\ClassInfo; /** @@ -11,37 +10,17 @@ use SilverStripe\Core\ClassInfo; */ class FlushRequestFilter implements RequestFilter { - - /** - * @inheritdoc - * - * @param HTTPRequest $request - * @param Session $session - * @param DataModel $model - * - * @return bool - */ - public function preRequest(HTTPRequest $request, Session $session, DataModel $model) + public function preRequest(HTTPRequest $request) { if (array_key_exists('flush', $request->getVars())) { foreach (ClassInfo::implementorsOf(Flushable::class) as $class) { $class::flush(); } } - return true; } - /** - * @inheritdoc - * - * @param HTTPRequest $request - * @param HTTPResponse $response - * @param DataModel $model - * - * @return bool - */ - public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model) + public function postRequest(HTTPRequest $request, HTTPResponse $response) { return true; } diff --git a/src/Control/HTTPRequest.php b/src/Control/HTTPRequest.php index 845e9a3df..f0b801ab5 100644 --- a/src/Control/HTTPRequest.php +++ b/src/Control/HTTPRequest.php @@ -125,6 +125,11 @@ class HTTPRequest implements ArrayAccess */ protected $unshiftedButParsedParts = 0; + /** + * @var Session + */ + protected $session; + /** * Construct a HTTPRequest from a URL relative to the site root. * @@ -138,12 +143,194 @@ class HTTPRequest implements ArrayAccess { $this->httpMethod = strtoupper(self::detect_method($httpMethod, $postVars)); $this->setUrl($url); - $this->getVars = (array) $getVars; $this->postVars = (array) $postVars; $this->body = $body; } + /** + * Create HTTPRequest instance from the current environment variables. + * May throw errors if request is invalid. + * + * @throws HTTPResponse_Exception + * @return static + */ + public static function createFromEnvironment() + { + // Health-check prior to creating environment + $variables = static::variablesFromEnvironment(); + return self::createFromVariables($variables, @file_get_contents('php://input')); + } + + /** + * Takes a $_SERVER data array and extracts HTTP request headers. + * + * @param array $server + * + * @return array + */ + public static function extractRequestHeaders(array $server) + { + $headers = array(); + foreach ($server as $key => $value) { + if (substr($key, 0, 5) == 'HTTP_') { + $key = substr($key, 5); + $key = strtolower(str_replace('_', ' ', $key)); + $key = str_replace(' ', '-', ucwords($key)); + $headers[$key] = $value; + } + } + + if (isset($server['CONTENT_TYPE'])) { + $headers['Content-Type'] = $server['CONTENT_TYPE']; + } + if (isset($server['CONTENT_LENGTH'])) { + $headers['Content-Length'] = $server['CONTENT_LENGTH']; + } + + return $headers; + } + + /** + * Clean up HTTP global vars for $_GET / $_REQUEST prior to bootstrapping + * Will also populate the $_GET['url'] var safely + * + * @param array $variables + * @return array Cleaned variables + */ + public static function cleanEnvironment(array $variables) + { + // IIS will sometimes generate this. + if (!empty($variables['_SERVER']['HTTP_X_ORIGINAL_URL'])) { + $variables['_SERVER']['REQUEST_URI'] = $variables['_SERVER']['HTTP_X_ORIGINAL_URL']; + } + + // Override REQUEST_METHOD + if (isset($variables['_SERVER']['X-HTTP-Method-Override'])) { + $variables['_SERVER']['REQUEST_METHOD'] = $variables['_SERVER']['X-HTTP-Method-Override']; + } + + // Prevent injection of url= querystring argument by prioritising any leading url argument + if (isset($variables['_SERVER']['QUERY_STRING']) && + preg_match('/^(?url=[^&?]*)(?.*[&?]url=.*)$/', $variables['_SERVER']['QUERY_STRING'], $results) + ) { + $queryString = $results['query'].'&'.$results['url']; + parse_str($queryString, $variables['_GET']); + } + + // Decode url from REQUEST_URI if not passed via $_GET['url'] + if (!isset($variables['_GET']['url'])) { + $url = $variables['_SERVER']['REQUEST_URI']; + + // Querystring args need to be explicitly parsed + if (strpos($url, '?') !== false) { + list($url, $queryString) = explode('?', $url, 2); + parse_str($queryString); + } + + // Ensure $_GET['url'] is set + $variables['_GET']['url'] = urldecode($url); + } + + // Remove base folders from the URL if webroot is hosted in a subfolder + if (substr(strtolower($variables['_GET']['url']), 0, strlen(BASE_URL)) === strtolower(BASE_URL)) { + $variables['_GET']['url'] = substr($variables['_GET']['url'], strlen(BASE_URL)); + } + + // Merge $_FILES into $_POST + $variables['_POST'] = array_merge((array)$variables['_POST'], (array)$variables['_FILES']); + + // Merge $_POST, $_GET, and $_COOKIE into $_REQUEST + $variables['_REQUEST'] = array_merge( + (array)$variables['_GET'], + (array)$variables['_POST'], + (array)$variables['_COOKIE'] + ); + + return $variables; + } + + /** + * Validate environment vars prior to building HTTPRequest + * Error conditions will raise HTTPResponse_Exceptions + * + * @throws HTTPResponse_Exception + * @return array + */ + protected static function variablesFromEnvironment() + { + // Validate $_FILES array before merging it with $_POST + foreach ($_FILES as $key => $value) { + if (is_array($value['tmp_name'])) { + $tempFiles = ArrayLib::array_values_recursive($value['tmp_name']); + } else { + $tempFiles = [ $value['tmp_name'] ]; + } + foreach ($tempFiles as $tmpFile) { + if ($tmpFile && !is_uploaded_file($tmpFile)) { + throw new HTTPResponse_Exception( + "File upload '{$key}' doesn't appear to be a valid upload", + 400 + ); + } + } + } + + // Validate hostname + if (getenv('SS_ALLOWED_HOSTS') && !Director::is_cli()) { + $all_allowed_hosts = explode(',', getenv('SS_ALLOWED_HOSTS')); + if (!in_array(Director::host(), $all_allowed_hosts)) { + throw new HTTPResponse_Exception('Invalid Host', 400); + } + } + + return [ + '_SERVER' => $_SERVER, + '_GET' => $_GET, + '_POST' => $_POST, + '_FILES' => $_FILES, + '_SESSION' => isset($_SESSION) ? $_SESSION : null, + '_COOKIE' => $_COOKIE + ]; + } + + /** + * Build HTTPRequest from given variables + * + * @param array $variables + * @param string $input Request body + * @return HTTPRequest + */ + public static function createFromVariables(array $variables, $input) + { + $variables = static::cleanEnvironment($variables); + + // Strip `url` out of querystring + $url = $variables['_GET']['url']; + unset($variables['_GET']['url']); + + // Build request + $request = new HTTPRequest( + $variables['_SERVER']['REQUEST_METHOD'], + $url, + $variables['_GET'], + $variables['_POST'], + $input + ); + + // Add headers + $headers = static::extractRequestHeaders($variables['_SERVER']); + foreach ($headers as $header => $value) { + $request->addHeader($header, $value); + } + + // Initiate an empty session - doesn't initialize an actual PHP session (see HTTPApplication) + $session = new Session($variables['_SESSION']); + $request->setSession($session); + + return $request; + } + /** * Allow the setting of a URL * @@ -435,21 +622,15 @@ class HTTPRequest implements ArrayAccess return $this->requestVar($offset); } - /** - * @ignore - * @param string $offset - * @param mixed $value - */ public function offsetSet($offset, $value) { + $this->getVars[$offset] = $value; } - /** - * @ignore - * @param mixed $offset - */ public function offsetUnset($offset) { + unset($this->getVars[$offset]); + unset($this->postVars[$offset]); } /** @@ -866,4 +1047,22 @@ class HTTPRequest implements ArrayAccess return $origMethod; } } + + /** + * @return Session + */ + public function getSession() + { + return $this->session; + } + + /** + * @param Session $session + * @return $this + */ + public function setSession(Session $session) + { + $this->session = $session; + return $this; + } } diff --git a/src/Control/RequestFilter.php b/src/Control/RequestFilter.php index a7fbd0b97..0d64980a8 100644 --- a/src/Control/RequestFilter.php +++ b/src/Control/RequestFilter.php @@ -2,8 +2,6 @@ namespace SilverStripe\Control; -use SilverStripe\ORM\DataModel; - /** * A request filter is an object that's executed before and after a * request occurs. By returning 'false' from the preRequest method, @@ -14,24 +12,20 @@ use SilverStripe\ORM\DataModel; */ interface RequestFilter { - /** * Filter executed before a request processes * * @param HTTPRequest $request Request container object - * @param Session $session Request session - * @param DataModel $model Current DataModel * @return boolean Whether to continue processing other filters. Null or true will continue processing (optional) */ - public function preRequest(HTTPRequest $request, Session $session, DataModel $model); + public function preRequest(HTTPRequest $request); /** * Filter executed AFTER a request * * @param HTTPRequest $request Request container object - * @param HTTPResponse $response Response output object - * @param DataModel $model Current DataModel - * @return boolean Whether to continue processing other filters. Null or true will continue processing (optional) + * @param HTTPResponse $response + * @return bool Whether to continue processing other filters. Null or true will continue processing (optional) */ - public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model); + public function postRequest(HTTPRequest $request, HTTPResponse $response); } diff --git a/src/Control/RequestHandler.php b/src/Control/RequestHandler.php index 0a087835f..86f4460aa 100644 --- a/src/Control/RequestHandler.php +++ b/src/Control/RequestHandler.php @@ -6,7 +6,6 @@ use InvalidArgumentException; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Dev\Debug; -use SilverStripe\ORM\DataModel; use SilverStripe\Security\Security; use SilverStripe\Security\PermissionFailureException; use SilverStripe\Security\Permission; @@ -123,22 +122,9 @@ class RequestHandler extends ViewableData $this->setRequest(new NullHTTPRequest()); - // This will prevent bugs if setDataModel() isn't called. - $this->model = DataModel::inst(); - parent::__construct(); } - /** - * Set the DataModel for this request. - * - * @param DataModel $model - */ - public function setDataModel($model) - { - $this->model = $model; - } - /** * Handles URL requests. * @@ -156,10 +142,9 @@ class RequestHandler extends ViewableData * customise the controller. * * @param HTTPRequest $request The object that is reponsible for distributing URL parsing - * @param DataModel $model * @return HTTPResponse|RequestHandler|string|array */ - public function handleRequest(HTTPRequest $request, DataModel $model) + public function handleRequest(HTTPRequest $request) { // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance if ($this->brokenOnConstruct) { @@ -170,7 +155,6 @@ class RequestHandler extends ViewableData } $this->setRequest($request); - $this->setDataModel($model); $match = $this->findAction($request); @@ -237,7 +221,7 @@ class RequestHandler extends ViewableData if ($result instanceof HasRequestHandler) { $result = $result->getRequestHandler(); } - $returnValue = $result->handleRequest($request, $model); + $returnValue = $result->handleRequest($request); // Array results can be used to handle if (is_array($returnValue)) { diff --git a/src/Control/RequestProcessor.php b/src/Control/RequestProcessor.php index 70e61e99c..a1e0cad1a 100644 --- a/src/Control/RequestProcessor.php +++ b/src/Control/RequestProcessor.php @@ -3,7 +3,6 @@ namespace SilverStripe\Control; use SilverStripe\Core\Injector\Injectable; -use SilverStripe\ORM\DataModel; /** * Represents a request processer that delegates pre and post request handling to nested request filters @@ -34,10 +33,10 @@ class RequestProcessor implements RequestFilter $this->filters = $filters; } - public function preRequest(HTTPRequest $request, Session $session, DataModel $model) + public function preRequest(HTTPRequest $request) { foreach ($this->filters as $filter) { - $res = $filter->preRequest($request, $session, $model); + $res = $filter->preRequest($request); if ($res === false) { return false; } @@ -45,10 +44,10 @@ class RequestProcessor implements RequestFilter return null; } - public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model) + public function postRequest(HTTPRequest $request, HTTPResponse $response) { foreach ($this->filters as $filter) { - $res = $filter->postRequest($request, $response, $model); + $res = $filter->postRequest($request, $response); if ($res === false) { return false; } diff --git a/src/Control/Session.php b/src/Control/Session.php index 4b9edeb7b..bf5f3f230 100644 --- a/src/Control/Session.php +++ b/src/Control/Session.php @@ -2,10 +2,8 @@ namespace SilverStripe\Control; -use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Injector\Injectable; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\Dev\Deprecation; +use BadMethodCallException; +use SilverStripe\Core\Config\Configurable; /** * Handles all manipulation of the session. @@ -89,7 +87,7 @@ use SilverStripe\Dev\Deprecation; */ class Session { - use Injectable; + use Configurable; /** * Set session timeout in seconds. @@ -130,12 +128,23 @@ class Session private static $cookie_secure = false; /** - * Session data + * Session data. + * Will be null if session has not been started + * + * @var array|null */ - protected $data = array(); + protected $data = null; + /** + * @var array + */ protected $changedData = array(); + /** + * Get user agent for this request + * + * @return string + */ protected function userAgent() { if (isset($_SERVER['HTTP_USER_AGENT'])) { @@ -148,123 +157,75 @@ class Session /** * Start PHP session, then create a new Session object with the given start data. * - * @param $data array|Session Can be an array of data (such as $_SESSION) or another Session object to clone. + * @param array|null|Session $data Can be an array of data (such as $_SESSION) or another Session object to clone. + * If null, this session is treated as unstarted. */ public function __construct($data) { if ($data instanceof Session) { - $data = $data->inst_getAll(); + $data = $data->getAll(); } $this->data = $data; + } + /** + * Init this session instance before usage + */ + public function init() + { + if (!$this->isStarted()) { + $this->start(); + } + + // Funny business detected! if (isset($this->data['HTTP_USER_AGENT'])) { - if ($this->data['HTTP_USER_AGENT'] != $this->userAgent()) { - // Funny business detected! - $this->inst_clearAll(); - $this->inst_destroy(); - $this->inst_start(); + if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent()) { + $this->clearAll(); + $this->destroy(); + $this->start(); } } } /** - * Add a value to a specific key in the session array - * - * @param string $name - * @param mixed $val + * Destroy existing session and restart */ - public static function add_to_array($name, $val) + public function restart() { - return self::current_session()->inst_addToArray($name, $val); + $this->destroy(); + $this->init(); } /** - * Set a key/value pair in the session + * Determine if this session has started * - * @param string $name Key - * @param string|array $val Value + * @return bool */ - public static function set($name, $val) + public function isStarted() { - return self::current_session()->inst_set($name, $val); + return isset($this->data); } /** - * Return a specific value by session key + * Begin session * - * @param string $name Key to lookup - * @return mixed + * @param string $sid */ - public static function get($name) + public function start($sid = null) { - return self::current_session()->inst_get($name); - } - - /** - * Return all the values in session - * - * @return array - */ - public static function get_all() - { - return self::current_session()->inst_getAll(); - } - - /** - * Clear a given session key, value pair. - * - * @param string $name Key to lookup - */ - public static function clear($name) - { - return self::current_session()->inst_clear($name); - } - - /** - * Clear all the values - * - * @return void - */ - public static function clear_all() - { - self::current_session()->inst_clearAll(); - self::$default_session = null; - } - - /** - * Save all the values in our session to $_SESSION - */ - public static function save() - { - return self::current_session()->inst_save(); - } - - protected static $default_session = null; - - protected static function current_session() - { - if (Controller::has_curr()) { - return Controller::curr()->getSession(); - } else { - if (!self::$default_session) { - self::$default_session = Injector::inst()->create('SilverStripe\\Control\\Session', isset($_SESSION) ? $_SESSION : array()); - } - - return self::$default_session; + if ($this->isStarted()) { + throw new BadMethodCallException("Session has already started"); } - } - public function inst_start($sid = null) - { - $path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path'); + $path = $this->config()->get('cookie_path'); if (!$path) { $path = Director::baseURL(); } - $domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain'); - $secure = Director::is_https() && Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure'); - $session_path = Config::inst()->get('SilverStripe\\Control\\Session', 'session_store_path'); - $timeout = Config::inst()->get('SilverStripe\\Control\\Session', 'timeout'); + $domain = $this->config()->get('cookie_domain'); + $secure = Director::is_https() && $this->config()->get('cookie_secure'); + $session_path = $this->config()->get('session_store_path'); + $timeout = $this->config()->get('timeout'); // Director::baseURL can return absolute domain names - this extracts the relevant parts // for the session otherwise we can get broken session cookies @@ -300,6 +261,8 @@ class Session session_start(); $this->data = isset($_SESSION) ? $_SESSION : array(); + } else { + $this->data = []; } // Modify the timeout behaviour so it's the *inactive* time before the session expires. @@ -310,28 +273,41 @@ class Session } } - public function inst_destroy($removeCookie = true) + /** + * Destroy this session + * + * @param bool $removeCookie + */ + public function destroy($removeCookie = true) { if (session_id()) { if ($removeCookie) { - $path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path') ?: Director::baseURL(); - $domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain'); - $secure = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure'); - + $path = $this->config()->get('cookie_path') ?: Director::baseURL(); + $domain = $this->config()->get('cookie_domain'); + $secure = $this->config()->get('cookie_secure'); Cookie::force_expiry(session_name(), $path, $domain, $secure, true); } - session_destroy(); - - // Clean up the superglobal - session_destroy does not do it. - // http://nz1.php.net/manual/en/function.session-destroy.php - unset($_SESSION); - $this->data = array(); } + // Clean up the superglobal - session_destroy does not do it. + // http://nz1.php.net/manual/en/function.session-destroy.php + unset($_SESSION); + $this->data = null; } - public function inst_set($name, $val) + /** + * Set session value + * + * @param string $name + * @param mixed $val + * @return $this + */ + public function set($name, $val) { + if (!$this->isStarted()) { + throw new BadMethodCallException("Session cannot be modified until it's started"); + } + // Quicker execution path for "."-free names if (strpos($name, '.') === false) { $this->data[$name] = $val; @@ -360,10 +336,21 @@ class Session $diffVar = $val; } } + return $this; } - public function inst_addToArray($name, $val) + /** + * Merge value with array + * + * @param string $name + * @param mixed $val + */ + public function addToArray($name, $val) { + if (!$this->isStarted()) { + throw new BadMethodCallException("Session cannot be modified until it's started"); + } + $names = explode('.', $name); // We still want to do this even if we have strict path checking for legacy code @@ -379,8 +366,18 @@ class Session $diffVar[sizeof($var)-1] = $val; } - public function inst_get($name) + /** + * Get session value + * + * @param string $name + * @return mixed + */ + public function get($name) { + if (!$this->isStarted()) { + throw new BadMethodCallException("Session cannot be accessed until it's started"); + } + // Quicker execution path for "."-free names if (strpos($name, '.') === false) { if (isset($this->data[$name])) { @@ -407,8 +404,18 @@ class Session } } - public function inst_clear($name) + /** + * Clear session value + * + * @param string $name + * @return $this + */ + public function clear($name) { + if (!$this->isStarted()) { + throw new BadMethodCallException("Session cannot be modified until it's started"); + } + $names = explode('.', $name); // We still want to do this even if we have strict path checking for legacy code @@ -418,7 +425,7 @@ class Session foreach ($names as $n) { // don't clear a record that doesn't exist if (!isset($var[$n])) { - return; + return $this; } $var = &$var[$n]; } @@ -432,38 +439,54 @@ class Session $var = null; $diffVar = null; } + return $this; } - public function inst_clearAll() + /** + * Clear all values + */ + public function clearAll() { + if (!$this->isStarted()) { + throw new BadMethodCallException("Session cannot be modified until it's started"); + } + if ($this->data && is_array($this->data)) { foreach (array_keys($this->data) as $key) { - $this->inst_clear($key); + $this->clear($key); } } } - public function inst_getAll() + /** + * Get all values + * + * @return array|null + */ + public function getAll() { return $this->data; } - public function inst_finalize() + /** + * Set user agent key + */ + public function finalize() { - $this->inst_set('HTTP_USER_AGENT', $this->userAgent()); + $this->set('HTTP_USER_AGENT', $this->userAgent()); } /** * Save data to session * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned. */ - public function inst_save() + public function save() { if ($this->changedData) { - $this->inst_finalize(); + $this->finalize(); - if (!isset($_SESSION)) { - $this->inst_start(); + if (!$this->isStarted()) { + $this->start(); } $this->recursivelyApply($this->changedData, $_SESSION); @@ -493,55 +516,11 @@ class Session /** * Return the changed data, for debugging purposes. + * * @return array */ - public function inst_changedData() + public function changedData() { return $this->changedData; } - - /** - * Sets the appropriate form message in session, with type. This will be shown once, - * for the form specified. - * - * @param string $formname the form name you wish to use ( usually $form->FormName() ) - * @param string $message the message you wish to add to it - * @param string $type the type of message - */ - public static function setFormMessage($formname, $message, $type) - { - Session::set("FormInfo.$formname.formError.message", $message); - Session::set("FormInfo.$formname.formError.type", $type); - } - - /** - * Is there a session ID in the request? - * @return bool - */ - public static function request_contains_session_id() - { - $secure = Director::is_https() && Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure'); - $name = $secure ? 'SECSESSID' : session_name(); - return isset($_COOKIE[$name]) || isset($_REQUEST[$name]); - } - - /** - * Initialize session. - * - * @param string $sid Start the session with a specific ID - */ - public static function start($sid = null) - { - self::current_session()->inst_start($sid); - } - - /** - * Destroy the active session. - * - * @param bool $removeCookie If set to TRUE, removes the user's cookie, FALSE does not remove - */ - public static function destroy($removeCookie = true) - { - self::current_session()->inst_destroy($removeCookie); - } } diff --git a/src/Core/AppKernel.php b/src/Core/AppKernel.php new file mode 100644 index 000000000..3e53ccf76 --- /dev/null +++ b/src/Core/AppKernel.php @@ -0,0 +1,413 @@ + SilverStripeServiceConfigurationLocator::class)); + $this->setContainer($injector); + Injector::set_inst($injector); + + // Manifest cache factory + $manifestCacheFactory = $this->buildManifestCacheFactory(); + + // Class loader + $classLoader = ClassLoader::inst(); + $classLoader->pushManifest(new ClassManifest(BASE_PATH, $manifestCacheFactory)); + $this->setClassLoader($classLoader); + + // Module loader + $moduleLoader = ModuleLoader::inst(); + $moduleManifest = new ModuleManifest(BASE_PATH, $manifestCacheFactory); + $moduleLoader->pushManifest($moduleManifest); + $this->setModuleLoader($moduleLoader); + + // Config loader + // @todo refactor CoreConfigFactory + $configFactory = new CoreConfigFactory($manifestCacheFactory, $this->getEnvironment()); + $configManifest = $configFactory->createRoot(); + $configLoader = ConfigLoader::inst(); + $configLoader->pushManifest($configManifest); + $this->setConfigLoader($configLoader); + + // Load template manifest + $themeResourceLoader = ThemeResourceLoader::inst(); + $themeResourceLoader->addSet('$default', new ThemeManifest( + BASE_PATH, + project(), + $manifestCacheFactory + )); + $this->setThemeResourceLoader($themeResourceLoader); + } + + public function getEnvironment() + { + // Check saved session + $env = $this->sessionEnvironment(); + if ($env) { + return $env; + } + + // Check set + if ($this->enviroment) { + return $this->enviroment; + } + + // Check getenv + if ($env = getenv('SS_ENVIRONMENT_TYPE')) { + return $env; + } + + return self::LIVE; + } + + /** + * Check or update any temporary environment specified in the session. + * + * @return null|string + */ + 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; + } + + // 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; + } + + /** + * @throws HTTPResponse_Exception + */ + public function boot() + { + $this->bootPHP(); + $this->bootManifests(); + $this->bootErrorHandling(); + $this->bootDatabase(); + } + + /** + * Configure database + * + * @throws HTTPResponse_Exception + */ + protected function bootDatabase() + { + // Check if a DB is named + $name = $this->getDatabaseName(); + + // Gracefully fail if no DB is configured + if (empty($name)) { + $this->detectLegacyEnvironment(); + $this->redirectToInstaller(); + } + + // Set default database config + $databaseConfig = $this->getDatabaseConfig(); + $databaseConfig['database'] = $this->getDatabaseName(); + DB::setConfig($databaseConfig); + } + + /** + * Check if there's a legacy _ss_environment.php file + * + * @throws HTTPResponse_Exception + */ + protected function detectLegacyEnvironment() + { + // Is there an _ss_environment.php file? + if (!file_exists(BASE_PATH . '/_ss_environment.php') && + !file_exists(dirname(BASE_PATH) . '/_ss_environment.php') + ) { + return; + } + + // Build error response + $dv = new DebugView(); + $body = + $dv->renderHeader() . + $dv->renderInfo( + "Configuraton Error", + Director::absoluteBaseURL() + ) . + $dv->renderParagraph( + 'You need to replace your _ss_environment.php file with a .env file, or with environment variables.

' + . 'See the ' + . 'Environment Management docs for more information.' + ) . + $dv->renderFooter(); + + // Raise error + $response = new HTTPResponse($body, 500); + throw new HTTPResponse_Exception($response); + } + + /** + * If missing configuration, redirect to install.php + */ + protected function redirectToInstaller() + { + // Error if installer not available + if (!file_exists(BASE_PATH . '/install.php')) { + throw new HTTPResponse_Exception( + 'SilverStripe Framework requires a $databaseConfig defined.', + 500 + ); + } + + // Redirect to installer + $response = new HTTPResponse(); + $response->redirect(Director::absoluteURL('install.php')); + throw new HTTPResponse_Exception($response); + } + + /** + * Load database config from environment + * + * @return array + */ + protected function getDatabaseConfig() + { + // Check global config + global $databaseConfig; + if (!empty($databaseConfig)) { + return $databaseConfig; + } + + /** @skipUpgrade */ + $databaseConfig = [ + "type" => getenv('SS_DATABASE_CLASS') ?: 'MySQLDatabase', + "server" => getenv('SS_DATABASE_SERVER') ?: 'localhost', + "username" => getenv('SS_DATABASE_USERNAME') ?: null, + "password" => getenv('SS_DATABASE_PASSWORD') ?: null, + ]; + + // Set the port if called for + $dbPort = getenv('SS_DATABASE_PORT'); + if ($dbPort) { + $databaseConfig['port'] = $dbPort; + } + + // Set the timezone if called for + $dbTZ = getenv('SS_DATABASE_TIMEZONE'); + if ($dbTZ) { + $databaseConfig['timezone'] = $dbTZ; + } + + // For schema enabled drivers: + $dbSchema = getenv('SS_DATABASE_SCHEMA'); + if ($dbSchema) { + $databaseConfig["schema"] = $dbSchema; + } + + // For SQlite3 memory databases (mainly for testing purposes) + $dbMemory = getenv('SS_DATABASE_MEMORY'); + if ($dbMemory) { + $databaseConfig["memory"] = $dbMemory; + } + + // Allow database adapters to handle their own configuration + DatabaseAdapterRegistry::autoconfigure(); + return $databaseConfig; + } + + /** + * Get name of database + * + * @return string + */ + protected function getDatabaseName() + { + $prefix = getenv('SS_DATABASE_PREFIX') ?: 'SS_'; + + // Check globals + global $database; + if (!empty($database)) { + return $prefix.$database; + } + global $databaseConfig; + if (!empty($databaseConfig['database'])) { + return $databaseConfig['database']; // Note: Already includes prefix + } + + // Check environment + $database = getenv('SS_DATABASE_NAME'); + if ($database) { + return $prefix.$database; + } + + // Auto-detect name + $chooseName = getenv('SS_DATABASE_CHOOSE_NAME'); + if ($chooseName) { + // Find directory to build name from + $loopCount = (int)$chooseName; + $databaseDir = BASE_PATH; + for ($i = 0; $i < $loopCount-1; $i++) { + $databaseDir = dirname($databaseDir); + } + + // Build name + $database = str_replace('.', '', basename($databaseDir)); + return $prefix.$database; + } + + // no DB name (may be optional for some connectors) + return null; + } + + /** + * Initialise PHP with default variables + */ + protected function bootPHP() + { + if ($this->getEnvironment() === self::LIVE) { + // limited to fatal errors and warnings in live mode + error_reporting(E_ALL & ~(E_DEPRECATED | E_STRICT | E_NOTICE)); + } else { + // Report all errors in dev / test mode + error_reporting(E_ALL | E_STRICT); + } + + global $_increase_time_limit_max; + $_increase_time_limit_max = -1; + + /** + * Ensure we have enough memory + */ + increase_memory_limit_to('64M'); + + /** + * Ensure we don't run into xdebug's fairly conservative infinite recursion protection limit + */ + increase_xdebug_nesting_level_to(200); + + /** + * Set default encoding + */ + mb_http_output('UTF-8'); + mb_internal_encoding('UTF-8'); + mb_regex_encoding('UTF-8'); + + /** + * Enable better garbage collection + */ + gc_enable(); + } + + /** + * @return ManifestCacheFactory + */ + protected function buildManifestCacheFactory() + { + return new ManifestCacheFactory([ + 'namespace' => 'manifestcache', + 'directory' => getTempFolder(), + ]); + } + + /** + * Boot all manifests + */ + protected function bootManifests() + { + // Regenerate the manifest if ?flush is set, or if the database is being built. + // The coupling is a hack, but it removes an annoying bug where new classes + // referenced in _config.php files can be referenced during the build process. + $flush = isset($_GET['flush']) || + trim($_GET['url'], '/') === trim(BASE_URL . '/dev/build', '/'); + + // Setup autoloader + $this->getClassLoader()->init(false, $flush); + + // Find modules + $this->getModuleLoader()->init(false, $flush); + + // Flush config + if ($flush) { + $config = $this->getConfigLoader()->getManifest(); + if ($config instanceof CachedConfigCollection) { + $config->setFlush($flush); + } + } + + // After loading config, boot _config.php files + $this->getModuleLoader()->getManifest()->activateConfig(); + + // Find default templates + $defaultSet = $this->getThemeResourceLoader()->getSet('$default'); + if ($defaultSet instanceof ThemeManifest) { + $defaultSet->init(false, $flush); + } + } + + /** + * Turn on error handling + */ + protected function bootErrorHandling() + { + // Register error handler + $errorHandler = Injector::inst()->get(ErrorHandler::class); + $errorHandler->start(); + + // Register error log file + $errorLog = getenv('SS_ERROR_LOG'); + if ($errorLog) { + $logger = Injector::inst()->get(LoggerInterface::class); + if ($logger instanceof Logger) { + $logger->pushHandler(new StreamHandler(BASE_PATH . '/' . $errorLog, Logger::WARNING)); + } else { + user_error("SS_ERROR_LOG setting only works with Monolog, you are using another logger", E_USER_WARNING); + } + } + } +} diff --git a/src/Core/Application.php b/src/Core/Application.php new file mode 100644 index 000000000..c24197e18 --- /dev/null +++ b/src/Core/Application.php @@ -0,0 +1,23 @@ +manifests)) { + throw new BadMethodCallException("No config manifests available"); + } return $this->manifests[count($this->manifests) - 1]; } diff --git a/src/Core/Config/CoreConfigFactory.php b/src/Core/Config/CoreConfigFactory.php index 3c1a9a217..338d8193d 100644 --- a/src/Core/Config/CoreConfigFactory.php +++ b/src/Core/Config/CoreConfigFactory.php @@ -2,22 +2,16 @@ namespace SilverStripe\Core\Config; -use Monolog\Handler\ErrorLogHandler; -use Monolog\Handler\StreamHandler; -use Monolog\Logger; -use Psr\Log\LoggerInterface; use Psr\SimpleCache\CacheInterface; use SilverStripe\Config\Collections\CachedConfigCollection; use SilverStripe\Config\Collections\MemoryConfigCollection; use SilverStripe\Config\Transformer\PrivateStaticTransformer; use SilverStripe\Config\Transformer\YamlTransformer; -use SilverStripe\Control\Director; use SilverStripe\Core\Cache\CacheFactory; use SilverStripe\Core\Config\Middleware\ExtensionMiddleware; use SilverStripe\Core\Config\Middleware\InheritanceMiddleware; use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ModuleLoader; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Finder\Finder; /** @@ -26,19 +20,19 @@ use Symfony\Component\Finder\Finder; class CoreConfigFactory { /** - * @var static + * @var CacheFactory */ - protected static $inst = null; + protected $cacheFactory = null; /** - * @return static + * @var string */ - public static function inst() + protected $environment = null; + + public function __construct(CacheFactory $cacheFactory, $environment) { - if (!self::$inst) { - self::$inst = new static(); - } - return self::$inst; + $this->cacheFactory = $cacheFactory; + $this->environment = $environment; } /** @@ -46,20 +40,17 @@ class CoreConfigFactory * This will be an immutable cached config, * which conditionally generates a nested "core" config. * - * @param bool $flush - * @param CacheFactory $cacheFactory * @return CachedConfigCollection */ - public function createRoot($flush, CacheFactory $cacheFactory) + public function createRoot() { $instance = new CachedConfigCollection(); // Create config cache - $cache = $cacheFactory->create(CacheInterface::class.'.configcache', [ + $cache = $this->cacheFactory->create(CacheInterface::class.'.configcache', [ 'namespace' => 'configcache' ]); $instance->setCache($cache); - $instance->setFlush($flush); // Set collection creator $instance->setCollectionCreator(function () { @@ -171,8 +162,7 @@ class CoreConfigFactory } ) ->addRule('environment', function ($env) { - $current = Director::get_environment_type(); - return strtolower($current) === strtolower($env); + return strtolower($this->environment) === strtolower($env); }) ->addRule('moduleexists', function ($module) { return ModuleLoader::inst()->getManifest()->moduleExists($module); diff --git a/src/Core/CoreKernel.php b/src/Core/CoreKernel.php new file mode 100644 index 000000000..4509c863a --- /dev/null +++ b/src/Core/CoreKernel.php @@ -0,0 +1,131 @@ +container; + } + + public function setContainer(Injector $container) + { + $this->container = $container; + $container->registerService($this, Kernel::class); + return $this; + } + + public function getClassLoader() + { + return $this->classLoader; + } + + public function setClassLoader(ClassLoader $classLoader) + { + $this->classLoader = $classLoader; + return $this; + } + + public function getModuleLoader() + { + return $this->moduleLoader; + } + + public function setModuleLoader(ModuleLoader $moduleLoader) + { + $this->moduleLoader = $moduleLoader; + return $this; + } + + public function getEnvironment() + { + return $this->enviroment ?: self::LIVE; + } + + public function setEnvironment($environment) + { + if (!in_array($environment, [self::DEV, self::TEST, self::LIVE])) { + throw new InvalidArgumentException( + "Director::set_environment_type passed '$environment'. It should be passed dev, test, or live" + ); + } + $this->enviroment = $environment; + return $this; + } + + public function getConfigLoader() + { + return $this->configLoader; + } + + public function setConfigLoader($configLoader) + { + $this->configLoader = $configLoader; + return $this; + } + + public function getThemeResourceLoader() + { + return $this->themeResourceLoader; + } + + public function setThemeResourceLoader($themeResourceLoader) + { + $this->themeResourceLoader = $themeResourceLoader; + return $this; + } +} diff --git a/src/Core/HTTPApplication.php b/src/Core/HTTPApplication.php new file mode 100644 index 000000000..abd24d5ad --- /dev/null +++ b/src/Core/HTTPApplication.php @@ -0,0 +1,115 @@ +middlewares; + } + + /** + * @param callable[] $middlewares + * @return $this + */ + public function setMiddlewares($middlewares) + { + $this->middlewares = $middlewares; + return $this; + } + + /** + * @param callable $middleware + * @return $this + */ + public function addMiddleware($middleware) + { + $this->middlewares[] = $middleware; + return $this; + } + + /** + * Call middleware + * + * @param callable $last Last config to call + * @return HTTPResponse + */ + protected function callMiddleware($last) + { + // Reverse middlewares + $next = $last; + foreach (array_reverse($this->getMiddlewares()) as $middleware) { + $next = function () use ($middleware, $next) { + return call_user_func($middleware, $next); + }; + } + return call_user_func($next); + } + + /** + * @var Kernel + */ + protected $kernel; + + public function __construct(Kernel $kernel) + { + $this->kernel = $kernel; + } + + /** + * Get the kernel for this application + * + * @return Kernel + */ + public function getKernel() + { + return $this->kernel; + } + + /** + * Handle the given HTTP request + * + * @param HTTPRequest $request + * @return HTTPResponse + */ + public function handle(HTTPRequest $request) + { + // Ensure boot is invoked + return $this->execute(function () use ($request) { + // Start session and execute + $request->getSession()->init(); + return Director::direct($request); + }); + } + + /** + * Safely boot the application and execute the given main action + * + * @param callable $callback + * @return HTTPResponse + */ + public function execute(callable $callback) + { + return $this->callMiddleware(function () use ($callback) { + // Pre-request boot + $this->getKernel()->boot(); + return call_user_func($callback); + }); + } +} diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php index 769c95a36..6e0b2bfca 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -202,21 +202,18 @@ class Injector implements ContainerInterface { $this->injectMap = array(); $this->serviceCache = array( - 'Injector' => $this, + 'Injector' => $this, ); - $this->specs = array( - 'Injector' => array('class' => 'SilverStripe\\Core\\Injector\\Injector') - ); - + $this->specs = [ + 'Injector' => ['class' => static::class] + ]; $this->autoProperties = array(); - - $creatorClass = isset($config['creator']) ? $config['creator'] - : 'SilverStripe\\Core\\Injector\\InjectionCreator'; + : InjectionCreator::class; $locatorClass = isset($config['locator']) ? $config['locator'] - : 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator'; + : SilverStripeServiceConfigurationLocator::class; $this->objectCreator = new $creatorClass; $this->configLocator = new $locatorClass; @@ -860,7 +857,6 @@ class Injector implements ContainerInterface $this->specs[$registerAt] = array('class' => get_class($service)); $this->serviceCache[$registerAt] = $service; - $this->inject($service); } /** diff --git a/src/Core/Injector/SilverStripeServiceConfigurationLocator.php b/src/Core/Injector/SilverStripeServiceConfigurationLocator.php index f6d7ea439..55020e2a3 100644 --- a/src/Core/Injector/SilverStripeServiceConfigurationLocator.php +++ b/src/Core/Injector/SilverStripeServiceConfigurationLocator.php @@ -51,7 +51,7 @@ class SilverStripeServiceConfigurationLocator implements ServiceConfigurationLoc return $this->configs[$name]; } - $config = Config::inst()->get('SilverStripe\\Core\\Injector\\Injector', $name); + $config = Config::inst()->get(Injector::class, $name); $this->configs[$name] = $config; return $config; } diff --git a/src/Core/Kernel.php b/src/Core/Kernel.php new file mode 100644 index 000000000..7b7d979f8 --- /dev/null +++ b/src/Core/Kernel.php @@ -0,0 +1,120 @@ +manifests as $manifest) { + /** @var ClassManifest $instance */ + $instance = $manifest['instance']; + $instance->init($includeTests, $forceRegen); + } + + $this->registerAutoloader(); + } + /** * Returns true if a class or interface name exists in the manifest. * diff --git a/src/Core/Manifest/ClassManifest.php b/src/Core/Manifest/ClassManifest.php index e64b9695a..244e630a3 100644 --- a/src/Core/Manifest/ClassManifest.php +++ b/src/Core/Manifest/ClassManifest.php @@ -28,6 +28,13 @@ class ClassManifest */ protected $base; + /** + * Used to build cache during boot + * + * @var CacheFactory + */ + protected $cacheFactory; + /** * Set if including test classes * @@ -56,7 +63,7 @@ class ClassManifest * * @var array */ - protected $classes = array(); + protected $classes = array(); /** * List of root classes with no parent class @@ -122,27 +129,30 @@ class ClassManifest * from the cache or re-scanning for classes. * * @param string $base The manifest base path. - * @param bool $includeTests Include the contents of "tests" directories. - * @param bool $forceRegen Force the manifest to be regenerated. * @param CacheFactory $cacheFactory Optional cache to use. Set to null to not cache. */ - public function __construct( - $base, - $includeTests = false, - $forceRegen = false, - CacheFactory $cacheFactory = null - ) { + public function __construct($base, CacheFactory $cacheFactory = null) + { $this->base = $base; - $this->tests = $includeTests; + $this->cacheFactory = $cacheFactory; + $this->cacheKey = 'manifest'; + } + /** + * Initialise the class manifest + * + * @param bool $includeTests + * @param bool $forceRegen + */ + public function init($includeTests = false, $forceRegen = false) + { // build cache from factory - if ($cacheFactory) { - $this->cache = $cacheFactory->create( + if ($this->cacheFactory) { + $this->cache = $this->cacheFactory->create( CacheInterface::class.'.classmanifest', [ 'namespace' => 'classmanifest' . ($includeTests ? '_tests' : '') ] ); } - $this->cacheKey = 'manifest'; if (!$forceRegen && $this->cache && ($data = $this->cache->get($this->cacheKey))) { $this->classes = $data['classes']; diff --git a/src/Core/Manifest/ModuleLoader.php b/src/Core/Manifest/ModuleLoader.php index 0c4152dee..be9a66718 100644 --- a/src/Core/Manifest/ModuleLoader.php +++ b/src/Core/Manifest/ModuleLoader.php @@ -85,4 +85,17 @@ class ModuleLoader { return count($this->manifests); } + + /** + * Initialise the module loader + * + * @param bool $includeTests + * @param bool $forceRegen + */ + public function init($includeTests = false, $forceRegen = false) + { + foreach ($this->manifests as $manifest) { + $manifest->init($includeTests, $forceRegen); + } + } } diff --git a/src/Core/Manifest/ModuleManifest.php b/src/Core/Manifest/ModuleManifest.php index eb9b8356e..04abc183e 100644 --- a/src/Core/Manifest/ModuleManifest.php +++ b/src/Core/Manifest/ModuleManifest.php @@ -26,11 +26,11 @@ class ModuleManifest protected $cacheKey; /** - * Whether `test` directories should be searched when searching for configuration + * Factory to use to build cache * - * @var bool + * @var CacheFactory */ - protected $includeTests; + protected $cacheFactory; /** * @var CacheInterface @@ -87,19 +87,24 @@ class ModuleManifest * from the cache or re-scanning for classes. * * @param string $base The project base path. - * @param bool $includeTests - * @param bool $forceRegen Force the manifest to be regenerated. * @param CacheFactory $cacheFactory Cache factory to use */ - public function __construct($base, $includeTests = false, $forceRegen = false, CacheFactory $cacheFactory = null) + public function __construct($base, CacheFactory $cacheFactory = null) { $this->base = $base; - $this->cacheKey = sha1($base).'_modules'; - $this->includeTests = $includeTests; + $this->cacheKey = sha1($base) . '_modules'; + $this->cacheFactory = $cacheFactory; + } + /** + * @param bool $includeTests + * @param bool $forceRegen Force the manifest to be regenerated. + */ + public function init($includeTests = false, $forceRegen = false) + { // build cache from factory - if ($cacheFactory) { - $this->cache = $cacheFactory->create( + if ($this->cacheFactory) { + $this->cache = $this->cacheFactory->create( CacheInterface::class.'.modulemanifest', [ 'namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '') ] ); diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php new file mode 100644 index 000000000..87c1b662c --- /dev/null +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -0,0 +1,146 @@ +application = $application; + $this->request = $request; + } + + /** + * @param callable $next + * @return HTTPResponse + */ + public function __invoke(callable $next) + { + $result = null; + + // Prepare tokens and execute chain + $reloadToken = ParameterConfirmationToken::prepare_tokens( + ['isTest', 'isDev', 'flush'], + $this->getRequest() + ); + $chain = new ErrorControlChain(); + $chain + ->then(function () use ($chain, $reloadToken, $next, &$result) { + // If no redirection is necessary then we can disable error supression + if (!$reloadToken) { + $chain->setSuppression(false); + } + + try { + // Check if a token is requesting a redirect + if ($reloadToken) { + $result = $this->safeReloadWithToken($reloadToken); + } else { + // If no reload necessary, process application + $result = call_user_func($next); + } + } catch (HTTPResponse_Exception $exception) { + $result = $exception->getResponse(); + } + }) + // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway + ->thenIfErrored(function () use ($reloadToken, &$result) { + if ($reloadToken) { + $result = $reloadToken->reloadWithToken(); + } + }) + ->execute(); + return $result; + } + + /** + * Reload application with the given token, but only if either the user is authenticated, + * or authentication is impossible. + * + * @param ParameterConfirmationToken $reloadToken + * @return HTTPResponse + */ + protected function safeReloadWithToken($reloadToken) + { + // Safe reload requires manual boot + $this->getApplication()->getKernel()->boot(); + + // Ensure session is started + $this->getRequest()->getSession()->init(); + + // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin + if (Director::isDev() || !Security::database_is_ready() || Permission::check('ADMIN')) { + return $reloadToken->reloadWithToken(); + } + + // Fail and redirect the user to the login page + $loginPage = Director::absoluteURL(Security::config()->get('login_url')); + $loginPage .= "?BackURL=" . urlencode($this->getRequest()->getURL()); + $result = new HTTPResponse(); + $result->redirect($loginPage); + return $result; + } + + /** + * @return Application + */ + public function getApplication() + { + return $this->application; + } + + /** + * @param Application $application + * @return $this + */ + public function setApplication(Application $application) + { + $this->application = $application; + return $this; + } + + /** + * @return HTTPRequest + */ + public function getRequest() + { + return $this->request; + } + + /** + * @param HTTPRequest $request + * @return $this + */ + public function setRequest(HTTPRequest $request) + { + $this->request = $request; + return $this; + } +} diff --git a/src/Core/Startup/OutputMiddleware.php b/src/Core/Startup/OutputMiddleware.php new file mode 100644 index 000000000..9a81432a2 --- /dev/null +++ b/src/Core/Startup/OutputMiddleware.php @@ -0,0 +1,37 @@ +defaultResponse = $defaultResponse; + } + + public function __invoke(callable $next) + { + /** @var HTTPResponse $response */ + $response = call_user_func($next); + if ($response) { + $response->output(); + } elseif ($this->defaultResponse) { + echo $this->defaultResponse; + } + return $response; + } +} diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 862ed4739..45b363097 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -2,7 +2,10 @@ namespace SilverStripe\Core\Startup; -use SilverStripe\Control\Director; +use SilverStripe\Control\Controller; +use SilverStripe\Control\HTTPRequest; +use SilverStripe\Control\HTTPResponse; +use SilverStripe\Core\Convert; use SilverStripe\Security\RandomGenerator; /** @@ -25,6 +28,11 @@ class ParameterConfirmationToken */ protected $parameterName = null; + /** + * @var HTTPRequest + */ + protected $request = null; + /** * The parameter given * @@ -88,17 +96,19 @@ class ParameterConfirmationToken * Create a new ParameterConfirmationToken * * @param string $parameterName Name of the querystring parameter to check + * @param HTTPRequest $request */ - public function __construct($parameterName) + public function __construct($parameterName, HTTPRequest $request) { // Store the parameter name $this->parameterName = $parameterName; + $this->request = $request; // Store the parameter value - $this->parameter = isset($_GET[$parameterName]) ? $_GET[$parameterName] : null; + $this->parameter = $request->getVar($parameterName); // If the token provided is valid, mark it as such - $token = isset($_GET[$parameterName.'token']) ? $_GET[$parameterName.'token'] : null; + $token = $request->getVar($parameterName.'token'); if ($this->checkToken($token)) { $this->token = $token; } @@ -151,7 +161,7 @@ class ParameterConfirmationToken */ public function suppress() { - unset($_GET[$this->parameterName]); + $this->request->offsetUnset($this->parameterName); } /** @@ -167,81 +177,45 @@ class ParameterConfirmationToken ); } - /** What to use instead of BASE_URL. Must not contain protocol or host. @var string */ - static public $alternateBaseURL = null; - - protected function currentAbsoluteURL() + /** + * Get redirect url, excluding querystring + * + * @return string + */ + protected function currentURL() { - global $url; - - // Are we http or https? Replicates Director::is_https() without its dependencies/ - $proto = 'http'; - // See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields - // See https://support.microsoft.com/en-us/kb/307347 - $headerOverride = false; - if (TRUSTED_PROXY) { - $headers = (getenv('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) ? array(getenv('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) : null; - if (!$headers) { - // Backwards compatible defaults - $headers = array('HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_PROTOCOL', 'HTTP_FRONT_END_HTTPS'); - } - foreach ($headers as $header) { - $headerCompareVal = ($header === 'HTTP_FRONT_END_HTTPS' ? 'on' : 'https'); - if (!empty($_SERVER[$header]) && strtolower($_SERVER[$header]) == $headerCompareVal) { - $headerOverride = true; - break; - } - } - } - - if ($headerOverride) { - $proto = 'https'; - } elseif ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) { - $proto = 'https'; - } elseif (isset($_SERVER['SSL'])) { - $proto = 'https'; - } - - $parts = array_filter(array( - // What's our host - Director::host(), - // SilverStripe base - self::$alternateBaseURL !== null ? self::$alternateBaseURL : BASE_URL, - // And URL including base script (eg: if it's index.php/page/url/) - (defined('BASE_SCRIPT_URL') ? '/' . BASE_SCRIPT_URL : '') . $url, - )); - - // Join together with protocol into our current absolute URL, avoiding duplicated "/" characters - return "$proto://" . preg_replace('#/{2,}#', '/', implode('/', $parts)); + return Controller::join_links( + BASE_URL, + '/', + $this->request->getURL(false) + ); } /** * Forces a reload of the request with the token included - * This method will terminate the script with `die` + * + * @return HTTPResponse */ public function reloadWithToken() { - $location = $this->currentAbsoluteURL(); + // Merge get params with current url + $params = array_merge($this->request->getVars(), $this->params()); + $location = Controller::join_links( + $this->currentURL(), + '?'.http_build_query($params) + ); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue the flush +HTML; - // What's our GET params (ensuring they include the original parameter + a new token) - $params = array_merge($_GET, $this->params()); - unset($params['url']); - - if ($params) { - $location .= '?'.http_build_query($params); - } - - // And redirect - if (headers_sent()) { - echo " - - -You are being redirected. If you are not redirected soon, click here to continue the flush -"; - } else { - header('location: '.$location, true, 302); - } - die; + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; } /** @@ -249,13 +223,14 @@ You are being redirected. If you are not redirected soon, cl * return the non-validated token with the highest priority * * @param array $keys List of token keys in ascending priority (low to high) + * @param HTTPRequest $request * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority */ - public static function prepare_tokens($keys) + public static function prepare_tokens($keys, HTTPRequest $request) { $target = null; foreach ($keys as $key) { - $token = new ParameterConfirmationToken($key); + $token = new ParameterConfirmationToken($key, $request); // Validate this token if ($token->reloadRequired()) { $token->suppress(); diff --git a/src/Core/TempPath.php b/src/Core/TempPath.php deleted file mode 100644 index a6b1cff6d..000000000 --- a/src/Core/TempPath.php +++ /dev/null @@ -1,111 +0,0 @@ -handleRequest($request, $this->model); + return $da->handleRequest($request); } else { $renderer = DebugView::create(); echo $renderer->renderHeader(); @@ -29,7 +29,7 @@ class DevBuildController extends Controller echo "
"; $da = DatabaseAdmin::create(); - $response = $da->handleRequest($request, $this->model); + $response = $da->handleRequest($request); echo "
"; echo $renderer->renderFooter(); diff --git a/src/Dev/FixtureBlueprint.php b/src/Dev/FixtureBlueprint.php index a511f86fd..bb753a8c0 100644 --- a/src/Dev/FixtureBlueprint.php +++ b/src/Dev/FixtureBlueprint.php @@ -2,13 +2,13 @@ namespace SilverStripe\Dev; -use SilverStripe\Assets\File; -use SilverStripe\ORM\DataModel; -use SilverStripe\ORM\DB; -use SilverStripe\ORM\DataObject; -use SilverStripe\Core\Config\Config; -use InvalidArgumentException; use Exception; +use InvalidArgumentException; +use SilverStripe\Assets\File; +use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\DB; /** * A blueprint on how to create instances of a certain {@link DataObject} subclass. @@ -94,7 +94,7 @@ class FixtureBlueprint try { $class = $this->class; $schema = DataObject::getSchema(); - $obj = DataModel::inst()->$class->newObject(); + $obj = Injector::inst()->create($class); // If an ID is explicitly passed, then we'll sort out the initial write straight away // This is just in case field setters triggered by the population code in the next block diff --git a/src/Dev/Install/php5-required.html b/src/Dev/Install/php5-required.html index 104cc275b..d27996a93 100644 --- a/src/Dev/Install/php5-required.html +++ b/src/Dev/Install/php5-required.html @@ -1,18 +1,18 @@ - PHP 5.5.0 is required + PHP 5.6.0 is required