From 8c8231c03e3fd1fea8d91da3c03676d59ed0847c Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 30 Jan 2017 15:35:43 +0000 Subject: [PATCH] NEW Director::host() to determine host name of site --- cli-script.php | 15 +++ .../03_Environment_Management.md | 1 + docs/en/02_Developer_Guides/17_CLI/index.md | 26 ++-- docs/en/04_Changelogs/4.0.0.md | 1 + .../05_Making_A_SilverStripe_Core_Release.md | 3 +- main.php | 3 +- src/Control/Director.php | 114 ++++++++++++------ src/Core/Constants.php | 74 ------------ .../Startup/ParameterConfirmationToken.php | 3 +- src/Dev/DevelopmentAdmin.php | 25 ---- tests/bootstrap/cli.php | 16 +++ tests/php/Dev/DevAdminControllerTest.php | 2 +- 12 files changed, 127 insertions(+), 156 deletions(-) diff --git a/cli-script.php b/cli-script.php index e3b2ac16a..7880e9084 100755 --- a/cli-script.php +++ b/cli-script.php @@ -20,6 +20,21 @@ if(PHP_SAPI != "cli" && PHP_SAPI != "cgi" && PHP_SAPI != "cgi-fcgi") { die(); } +// We update the $_SERVER variable to contain data consistent with the rest of the application. +$_SERVER = array_merge(array( + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'HTTP_ACCEPT' => 'text/plain;q=0.5', + 'HTTP_ACCEPT_LANGUAGE' => '*;q=0.5', + 'HTTP_ACCEPT_ENCODING' => '', + 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1;q=0.5', + 'SERVER_SIGNATURE' => 'Command-line PHP/' . phpversion(), + 'SERVER_SOFTWARE' => 'PHP/' . phpversion(), + 'SERVER_ADDR' => '127.0.0.1', + 'REMOTE_ADDR' => '127.0.0.1', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => 'CLI', +), $_SERVER); + /** * Identify the cli-script.php file and change to its container directory, so that require_once() works */ diff --git a/docs/en/00_Getting_Started/03_Environment_Management.md b/docs/en/00_Getting_Started/03_Environment_Management.md index 9a817a293..289e3e20b 100644 --- a/docs/en/00_Getting_Started/03_Environment_Management.md +++ b/docs/en/00_Getting_Started/03_Environment_Management.md @@ -87,3 +87,4 @@ SilverStripe core environment variables are listed here, though you're free to d | `SS_ALLOWED_HOSTS` | A comma deliminated list of hostnames the site is allowed to respond to | | `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching) | | `SS_IGNORE_DOT_ENV` | If set the .env file will be ignored. This is good for live to mitigate any performance implications of loading the .env file | +| `SS_HOST` | The hostname to use when it isn't determinable by other means (eg: for CLI commands) | diff --git a/docs/en/02_Developer_Guides/17_CLI/index.md b/docs/en/02_Developer_Guides/17_CLI/index.md index f1b02fc31..aeb63d505 100644 --- a/docs/en/02_Developer_Guides/17_CLI/index.md +++ b/docs/en/02_Developer_Guides/17_CLI/index.md @@ -44,25 +44,21 @@ This currently only works on UNIX like systems, not on Windows. Sometimes SilverStripe needs to know the URL of your site. For example, when sending an email or generating static files. When you're visiting the site in a web browser this is easy to work out, but when executing scripts on the -command line, it has no way of knowing. To work this out, add lines to your -[_ss_environment.php](/getting_started/environment_management) file. +command line, it has no way of knowing. To work this out, there are several ways to resolve this. You can set alternate +base URLs, hosts, and protocol. - :::php - global $_FILE_TO_URL_MAPPING; +eg: - $_FILE_TO_URL_MAPPING['/Users/sminnee/Sites'] = 'http://localhost'; +```yml +SilverStripe\Control\Director: + alternate_base_url: 'https://example.com/' +``` -The above statement tells SilverStripe that anything executed under the `/Users/sminnee/Sites` directory will have the -base URL `http://localhost`. The site `/Users/sminnee/Sites/my_silverstripe_project` will translate to the URL -`http://localhost/my_silverstripe_project`. +Alternatively you can use the `SS_HOST` environment variable to set a fallback hostname: -You can add multiple file to url mapping definitions. The most specific mapping will be used. - - :::php - global $_FILE_TO_URL_MAPPING; - - $_FILE_TO_URL_MAPPING['/Users/sminnee/Sites'] = 'http://localhost'; - $_FILE_TO_URL_MAPPING['/Users/sminnee/Sites/my_silverstripe_project'] = 'http://project.localhost'; +``` +SS_HOST="localhost" +``` ### Usage diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 8db4ea5fe..005510064 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -1352,3 +1352,4 @@ logic early in the bootstrap, this is best placed in the `_config.php` files. * Removed support for _ss_environment.php in favour of .env and first class environment variables * Environment variables now can be set in `.env` file placed in webroot or one level above * Environment variables will be read from the environment as well +* `$_FILE_TO_URL_MAPPING` has been removed and replaced with using `Director.alternate_host` or `SS_HOST` env var diff --git a/docs/en/05_Contributing/05_Making_A_SilverStripe_Core_Release.md b/docs/en/05_Contributing/05_Making_A_SilverStripe_Core_Release.md index 98d87a546..81ed1aaac 100644 --- a/docs/en/05_Contributing/05_Making_A_SilverStripe_Core_Release.md +++ b/docs/en/05_Contributing/05_Making_A_SilverStripe_Core_Release.md @@ -58,8 +58,7 @@ Example `.env`: SS_DEFAULT_ADMIN_PASSWORD="password" # Basic CLI hostname - global $_FILE_TO_URL_MAPPING; - $_FILE_TO_URL_MAPPING[__DIR__] = "http://localhost"; + SS_HOST="localhost"; You will also need to be assigned the following permissions. Contact one of the SS staff from diff --git a/main.php b/main.php index c1b121b11..daa214c59 100644 --- a/main.php +++ b/main.php @@ -188,8 +188,9 @@ if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseC 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://" . $_SERVER['HTTP_HOST'] . BASE_URL . '/install.php'; + $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 diff --git a/src/Control/Director.php b/src/Control/Director.php index b9c6e0366..ba8fc2ac5 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -101,6 +101,13 @@ class Director implements TemplateGlobalProvider */ private static $alternate_protocol; + /** + * @config + * + * @var string + */ + private static $alternate_host; + /** * @config * @@ -139,6 +146,15 @@ class Director implements TemplateGlobalProvider */ public static function direct($url, DataModel $model) { + // 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'])) { @@ -551,6 +567,62 @@ class Director implements TemplateGlobalProvider return Controller::join_links($parent, $url); } + /** + * A helper to determine the current hostname used to access the site. + * The following are used to determine the host (in order) + * - Director.alternate_host + * - Director.alternate_base_url (if it contains a domain name) + * - Trusted proxy headers + * - HTTP Host header + * - SS_HOST env var + * - SERVER_NAME + * - gethostname() + * + * @return string + */ + public static function host() + { + $headerOverride = false; + if (TRUSTED_PROXY) { + $headers = (getenv('SS_TRUSTED_PROXY_HOST_HEADER')) ? array(getenv('SS_TRUSTED_PROXY_HOST_HEADER')) : null; + if (!$headers) { + // Backwards compatible defaults + $headers = array('HTTP_X_FORWARDED_HOST'); + } + foreach ($headers as $header) { + if (!empty($_SERVER[$header])) { + // Get the first host, in case there's multiple separated through commas + $headerOverride = strtok($_SERVER[$header], ','); + break; + } + } + } + + if ($host = static::config()->get('alternate_host')) { + return $host; + } + + if ($baseURL = static::config()->get('alternate_base_url')) { + if (preg_match('/^(http[^:]*:\/\/[^\/]+)(\/|$)/', $baseURL, $matches)) { + return parse_url($baseURL, PHP_URL_HOST); + } + } + + if ($headerOverride) { + return $headerOverride; + } + + if (isset($_SERVER['HTTP_HOST'])) { + return $_SERVER['HTTP_HOST']; + } + + if ($host = getenv('SS_HOST')) { + return $host; + } + + return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname(); + } + /** * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment * variable isn't set. @@ -559,31 +631,7 @@ class Director implements TemplateGlobalProvider */ public static function protocolAndHost() { - $alternate = Config::inst()->get('SilverStripe\\Control\\Director', 'alternate_base_url'); - if ($alternate) { - if (preg_match('/^(http[^:]*:\/\/[^\/]+)(\/|$)/', $alternate, $matches)) { - return $matches[1]; - } - } - - if (isset($_SERVER['HTTP_HOST'])) { - return Director::protocol() . $_SERVER['HTTP_HOST']; - } else { - global $_FILE_TO_URL_MAPPING; - if (Director::is_cli() && isset($_FILE_TO_URL_MAPPING)) { - $errorSuggestion = ' You probably want to define ' . - 'an entry in $_FILE_TO_URL_MAPPING that covers "' . Director::baseFolder() . '"'; - } elseif (Director::is_cli()) { - $errorSuggestion = ' You probably want to define $_FILE_TO_URL_MAPPING in ' . - 'your _ss_environment.php as instructed on the "sake" page of the doc.silverstripe.com wiki'; - } else { - $errorSuggestion = ""; - } - - user_error("Director::protocolAndHost() lacks sufficient information - HTTP_HOST not set." - . $errorSuggestion, E_USER_WARNING); - return false; - } + return static::protocol() . static::host(); } /** @@ -950,7 +998,7 @@ class Director implements TemplateGlobalProvider $login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@"; } - return Director::protocol() . $login . $_SERVER['HTTP_HOST'] . Director::baseURL(); + return Director::protocol() . $login . static::host() . Director::baseURL(); } /** @@ -1052,7 +1100,7 @@ class Director implements TemplateGlobalProvider */ public static function forceWWW() { - if (!Director::isDev() && !Director::isTest() && strpos($_SERVER['HTTP_HOST'], 'www') !== 0) { + if (!Director::isDev() && !Director::isTest() && strpos(static::host(), 'www') !== 0) { $destURL = str_replace( Director::protocol(), Director::protocol() . 'www.', @@ -1191,11 +1239,7 @@ class Director implements TemplateGlobalProvider // Check if we are running on one of the test servers $devServers = (array)Config::inst()->get('SilverStripe\\Control\\Director', 'dev_servers'); - if (isset($_SERVER['HTTP_HOST']) && in_array($_SERVER['HTTP_HOST'], $devServers)) { - return true; - } - - return false; + return in_array(static::host(), $devServers); } /** @@ -1223,11 +1267,7 @@ class Director implements TemplateGlobalProvider // Check if we are running on one of the test servers $testServers = (array)Config::inst()->get('SilverStripe\\Control\\Director', 'test_servers'); - if (isset($_SERVER['HTTP_HOST']) && in_array($_SERVER['HTTP_HOST'], $testServers)) { - return true; - } - - return false; + return in_array(static::host(), $testServers); } /** diff --git a/src/Core/Constants.php b/src/Core/Constants.php index fb1188815..46a9153b0 100644 --- a/src/Core/Constants.php +++ b/src/Core/Constants.php @@ -61,80 +61,6 @@ if (!defined('TRUSTED_PROXY')) { define('TRUSTED_PROXY', $trusted); } -/** - * A blank HTTP_HOST value is used to detect command-line execution. - * We update the $_SERVER variable to contain data consistent with the rest of the application. - */ -if (!isset($_SERVER['HTTP_HOST'])) { - // HTTP_HOST, REQUEST_PORT, SCRIPT_NAME, and PHP_SELF - global $_FILE_TO_URL_MAPPING; - if (isset($_FILE_TO_URL_MAPPING)) { - $fullPath = $testPath = realpath($_SERVER['SCRIPT_FILENAME']); - while ($testPath && $testPath != '/' && !preg_match('/^[A-Z]:\\\\$/', $testPath)) { - if (isset($_FILE_TO_URL_MAPPING[$testPath])) { - $url = $_FILE_TO_URL_MAPPING[$testPath] - . str_replace(DIRECTORY_SEPARATOR, '/', substr($fullPath, strlen($testPath))); - - $components = parse_url($url); - $_SERVER['HTTP_HOST'] = $components['host']; - if (!empty($components['port'])) { - $_SERVER['HTTP_HOST'] .= ':' . $components['port']; - } - $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'] = $components['path']; - if (!empty($components['port'])) { - $_SERVER['REQUEST_PORT'] = $components['port']; - } - break; - } - $testPath = dirname($testPath); - } - } - - // Everything else - $serverDefaults = array( - 'SERVER_PROTOCOL' => 'HTTP/1.1', - 'HTTP_ACCEPT' => 'text/plain;q=0.5', - 'HTTP_ACCEPT_LANGUAGE' => '*;q=0.5', - 'HTTP_ACCEPT_ENCODING' => '', - 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1;q=0.5', - 'SERVER_SIGNATURE' => 'Command-line PHP/' . phpversion(), - 'SERVER_SOFTWARE' => 'PHP/' . phpversion(), - 'SERVER_ADDR' => '127.0.0.1', - 'REMOTE_ADDR' => '127.0.0.1', - 'REQUEST_METHOD' => 'GET', - 'HTTP_USER_AGENT' => 'CLI', - ); - - $_SERVER = array_merge($serverDefaults, $_SERVER); - - /** - * If we have an HTTP_HOST value, then we're being called from the webserver and there are some things that - * need checking - */ -} else { - - /** - * Fix HTTP_HOST from reverse proxies - */ - $trustedProxyHeader = (defined('SS_TRUSTED_PROXY_HOST_HEADER')) - ? SS_TRUSTED_PROXY_HOST_HEADER - : 'HTTP_X_FORWARDED_HOST'; - - if (TRUSTED_PROXY && !empty($_SERVER[$trustedProxyHeader])) { - // Get the first host, in case there's multiple separated through commas - $_SERVER['HTTP_HOST'] = strtok($_SERVER[$trustedProxyHeader], ','); - } -} - -// Filter by configured allowed hosts -if (defined('SS_ALLOWED_HOSTS') && php_sapi_name() !== "cli") { - $all_allowed_hosts = explode(',', SS_ALLOWED_HOSTS); - if (!isset($_SERVER['HTTP_HOST']) || !in_array($_SERVER['HTTP_HOST'], $all_allowed_hosts)) { - header('HTTP/1.1 400 Invalid Host', true, 400); - die(); - } -} - /** * Define system paths */ diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index b9075614a..862ed4739 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -2,6 +2,7 @@ namespace SilverStripe\Core\Startup; +use SilverStripe\Control\Director; use SilverStripe\Security\RandomGenerator; /** @@ -203,7 +204,7 @@ class ParameterConfirmationToken $parts = array_filter(array( // What's our host - $_SERVER['HTTP_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/) diff --git a/src/Dev/DevelopmentAdmin.php b/src/Dev/DevelopmentAdmin.php index e8bc4124e..9b8bd28e6 100644 --- a/src/Dev/DevelopmentAdmin.php +++ b/src/Dev/DevelopmentAdmin.php @@ -64,31 +64,6 @@ class DevelopmentAdmin extends Controller return; } - // check for valid url mapping - // lacking this information can cause really nasty bugs, - // e.g. when running Director::test() from a FunctionalTest instance - global $_FILE_TO_URL_MAPPING; - if (Director::is_cli()) { - if (isset($_FILE_TO_URL_MAPPING)) { - $testPath = BASE_PATH; - $matched = false; - while ($testPath && $testPath != "/" && !preg_match('/^[A-Z]:\\\\$/', $testPath)) { - if (isset($_FILE_TO_URL_MAPPING[$testPath])) { - $matched = true; - break; - } - $testPath = dirname($testPath); - } - if (!$matched) { - echo 'Warning: You probably want to define '. - 'an entry in $_FILE_TO_URL_MAPPING that covers "' . Director::baseFolder() . '"' . "\n"; - } - } else { - echo 'Warning: You probably want to define $_FILE_TO_URL_MAPPING in '. - 'your _ss_environment.php as instructed on the "sake" page of the doc.silverstripe.org wiki'."\n"; - } - } - // Backwards compat: Default to "draft" stage, which is important // for tasks like dev/build which call DataObject->requireDefaultRecords(), // but also for other administrative tasks which have assumptions about the default stage. diff --git a/tests/bootstrap/cli.php b/tests/bootstrap/cli.php index b3fb74fed..dbc3d52d1 100644 --- a/tests/bootstrap/cli.php +++ b/tests/bootstrap/cli.php @@ -6,6 +6,22 @@ if (!$_SERVER) { $_SERVER = array(); } +// We update the $_SERVER variable to contain data consistent with the rest of the application. +$_SERVER = array_merge(array( + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'HTTP_ACCEPT' => 'text/plain;q=0.5', + 'HTTP_ACCEPT_LANGUAGE' => '*;q=0.5', + 'HTTP_ACCEPT_ENCODING' => '', + 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1;q=0.5', + 'SERVER_SIGNATURE' => 'Command-line PHP/' . phpversion(), + 'SERVER_SOFTWARE' => 'PHP/' . phpversion(), + 'SERVER_NAME' => 'localhost', + 'SERVER_ADDR' => '127.0.0.1', + 'REMOTE_ADDR' => '127.0.0.1', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => 'CLI', +), $_SERVER); + $frameworkPath = dirname(dirname(__FILE__)); $frameworkDir = basename($frameworkPath); diff --git a/tests/php/Dev/DevAdminControllerTest.php b/tests/php/Dev/DevAdminControllerTest.php index e8de7540f..ab8210ff9 100644 --- a/tests/php/Dev/DevAdminControllerTest.php +++ b/tests/php/Dev/DevAdminControllerTest.php @@ -42,7 +42,7 @@ class DevAdminControllerTest extends FunctionalTest public function testGoodRegisteredControllerOutput() { // Check for the controller running from the registered url above - // (we use contains rather than equals because sometimes you get Warning: You probably want to define an entry in $_FILE_TO_URL_MAPPING) + // (we use contains rather than equals because sometimes you get a warning) $this->assertContains(Controller1::OK_MSG, $this->getCapture('/dev/x1')); $this->assertContains(Controller1::OK_MSG, $this->getCapture('/dev/x1/y1')); }