From 0a9ec3fc423cb3487f4707ac57c23e9aeac2e33d Mon Sep 17 00:00:00 2001
From: Daniel Hensby <dhensby@silverstripe.com>
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/04_Changelogs/4.0.0.md                |   1 +
 .../05_Making_A_SilverStripe_Core_Release.md  |   3 +-
 main.php                                      |   3 +-
 src/Control/Director.php                      | 110 ++++++++++++------
 src/Core/Constants.php                        |  74 ------------
 .../Startup/ParameterConfirmationToken.php    |   3 +-
 src/Dev/DevelopmentAdmin.php                  |  25 ----
 tests/bootstrap/cli.php                       |  16 +++
 10 files changed, 113 insertions(+), 138 deletions(-)

diff --git a/cli-script.php b/cli-script.php
index e3b2ac16a73..7880e9084d7 100755
--- a/cli-script.php
+++ b/cli-script.php
@@ -20,6 +20,21 @@
 	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 9a817a29305..289e3e20b39 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/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md
index 8db4ea5fea0..00551006459 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 98d87a54695..81ed1aaac83 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 c1b121b1196..daa214c5984 100644
--- a/main.php
+++ b/main.php
@@ -188,8 +188,9 @@
 		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 b9c6e036604..ba8fc2ac503 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'])) {
@@ -552,38 +568,70 @@ public static function absoluteURL($url, $relativeParent = self::BASE)
     }
 
     /**
-     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
-     * variable isn't set.
+     * 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 bool|string
+     * @return string
      */
-    public static function protocolAndHost()
+    public static function host()
     {
-        $alternate = Config::inst()->get('SilverStripe\\Control\\Director', 'alternate_base_url');
-        if ($alternate) {
-            if (preg_match('/^(http[^:]*:\/\/[^\/]+)(\/|$)/', $alternate, $matches)) {
-                return $matches[1];
+        $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 (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 = "";
+        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);
             }
+        }
 
-            user_error("Director::protocolAndHost() lacks sufficient information - HTTP_HOST not set."
-                . $errorSuggestion, E_USER_WARNING);
-            return false;
+        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.
+     *
+     * @return bool|string
+     */
+    public static function protocolAndHost()
+    {
+        return static::protocol() . static::host();
     }
 
     /**
@@ -950,7 +998,7 @@ public static function absoluteBaseURLWithAuth()
             $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 @@ public static function forceSSL($patterns = null, $secureDomain = null)
      */
     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 @@ public static function isDev()
 
         // 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 @@ public static function isTest()
 
         // 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 fb118881577..46a9153b0a6 100644
--- a/src/Core/Constants.php
+++ b/src/Core/Constants.php
@@ -61,80 +61,6 @@
     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 b9075614a12..862ed4739a5 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 @@ protected function currentAbsoluteURL()
 
         $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 e8bc4124e6e..9b8bd28e63f 100644
--- a/src/Dev/DevelopmentAdmin.php
+++ b/src/Dev/DevelopmentAdmin.php
@@ -64,31 +64,6 @@ protected function init()
             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 b3fb74fed79..dbc3d52d1ec 100644
--- a/tests/bootstrap/cli.php
+++ b/tests/bootstrap/cli.php
@@ -6,6 +6,22 @@
     $_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);