Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

Director   F

Complexity

Total Complexity 192

Size/Duplication

Total Lines 1082
Duplicated Lines 5.73 %

Coupling/Cohesion

Components 2
Dependencies 16

Importance

Changes 0
Metric Value
dl 62
loc 1082
rs 0.7298
c 0
b 0
f 0
wmc 192
lcom 2
cbo 16

37 Methods

Rating   Name   Duplication   Size   Complexity  
A addRules() 0 5 1
D direct() 0 93 21
F test() 3 125 24
C handleRequest() 0 52 12
A setUrlParams() 0 3 1
A get_current_page() 0 3 2
A set_current_page() 0 3 1
C absoluteURL() 0 24 10
C protocolAndHost() 0 24 7
A protocol() 0 3 2
C is_https() 14 35 13
B baseURL() 0 21 6
A setBaseURL() 0 4 1
A baseFolder() 0 4 2
A setBaseFolder() 0 4 1
C makeRelative() 6 47 10
A is_absolute() 0 4 3
C is_absolute_url() 6 25 7
A is_relative_url() 0 3 1
A is_site_url() 0 9 4
B extract_request_headers() 0 17 5
A getAbsFile() 0 3 2
A fileExists() 0 5 1
A absoluteBaseURL() 0 3 1
A absoluteBaseURLWithAuth() 0 8 2
A force_redirect() 0 10 1
C forceSSL() 0 44 11
A forceWWW() 0 8 4
A is_ajax() 0 10 4
A is_cli() 0 3 1
A set_environment_type() 0 9 4
A get_environment_type() 0 11 4
A isLive() 0 3 2
B isDev() 15 15 5
B isTest() 18 18 6
C session_environment() 0 24 9
A get_template_global_variables() 0 9 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Director often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Director, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Director is responsible for processing URLs, and providing environment information.
4
 *
5
 * The most important part of director is {@link Director::direct()}, which is passed a URL and will execute the
6
 * appropriate controller.
7
 *
8
 * Director also has a number of static methods that provide information about the environment, such as
9
 * {@link Director::$environment_type}.
10
 *
11
 * @package framework
12
 * @subpackage control
13
 * @see Director::direct()
14
 * @see Director::$rules
15
 * @see Director::$environment_type
16
 */
17
class Director implements TemplateGlobalProvider {
18
19
	static private $urlParams;
20
21
	static private $rules = array();
22
23
	/**
24
	 * @var SiteTree
25
	 */
26
	private static $current_page;
27
28
	/**
29
	 * @config
30
	 * @var string
31
	 */
32
	private static $alternate_base_folder;
33
34
	/**
35
	 * @config
36
	 * @var array
37
	 */
38
	private static $dev_servers = array();
39
40
	/**
41
	 * @config
42
	 * @var array
43
	 */
44
	private static $test_servers = array();
45
46
	/**
47
	 * Setting this explicitly specifies the protocol (http or https) used, overriding
48
	 * the normal behaviour of Director::is_https introspecting it from the request
49
	 *
50
	 * @config
51
	 * @var string - "http" or "https" to force the protocol, or false-ish to use default introspection from request
52
	 */
53
	private static $alternate_protocol;
54
55
	/**
56
	 * @config
57
	 * @var string
58
	 */
59
	private static $alternate_base_url;
60
61
	/**
62
	 * @config
63
	 * @var string
64
	 */
65
	private static $environment_type;
66
67
	/**
68
	 * Add URL matching rules to the Director.
69
	 *
70
	 * The director is responsible for turning URLs into Controller objects.
71
	 *
72
	 * @deprecated 4.0 Use the "Director.rules" config setting instead
73
	 * @param $priority The priority of the rules; higher values will get your rule checked first.  We recommend
74
	 *                  priority 100 for your site's rules.  The built-in rules are priority 10, standard modules are
75
	 *                  priority 50.
76
	 */
77
	public static function addRules($priority, $rules) {
0 ignored issues
show
Unused Code introduced by
The parameter $priority is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
78
		Deprecation::notice('4.0', 'Use the "Director.rules" config setting instead');
79
80
		Config::inst()->update('Director', 'rules', $rules);
81
	}
82
83
	/**
84
	 * Process the given URL, creating the appropriate controller and executing it.
85
	 *
86
	 * Request processing is handled as follows:
87
	 *  - Director::direct() creates a new SS_HTTPResponse object and passes this to Director::handleRequest().
88
	 *  - Director::handleRequest($request) checks each of the Director rules and identifies a controller to handle
89
	 *    this request.
90
	 *  - Controller::handleRequest($request) is then called.  This will find a rule to handle the URL, and call the
91
	 *    rule handling method.
92
	 *  - RequestHandler::handleRequest($request) is recursively called whenever a rule handling method returns a
93
	 *    RequestHandler object.
94
	 *
95
	 * In addition to request processing, Director will manage the session, and perform the output of the actual
96
	 * response to the browser.
97
	 *
98
	 * @param $url String, the URL the user is visiting, without the querystring.
99
	 * @uses handleRequest() rule-lookup logic is handled by this.
100
	 * @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
101
	 */
102
	public static function direct($url, DataModel $model) {
103
		// Validate $_FILES array before merging it with $_POST
104
		foreach($_FILES as $k => $v) {
105
			if(is_array($v['tmp_name'])) {
106
				$v = ArrayLib::array_values_recursive($v['tmp_name']);
107
				foreach($v as $tmpFile) {
108
					if($tmpFile && !is_uploaded_file($tmpFile)) {
109
						user_error("File upload '$k' doesn't appear to be a valid upload", E_USER_ERROR);
110
					}
111
				}
112
			} else {
113
				if($v['tmp_name'] && !is_uploaded_file($v['tmp_name'])) {
114
					user_error("File upload '$k' doesn't appear to be a valid upload", E_USER_ERROR);
115
				}
116
			}
117
		}
118
119
		$req = new SS_HTTPRequest(
120
			(isset($_SERVER['X-HTTP-Method-Override']))
121
				? $_SERVER['X-HTTP-Method-Override']
122
				: $_SERVER['REQUEST_METHOD'],
123
			$url,
124
			$_GET,
125
			ArrayLib::array_merge_recursive((array) $_POST, (array) $_FILES),
0 ignored issues
show
Bug introduced by
It seems like \ArrayLib::array_merge_r..._POST, (array) $_FILES) targeting ArrayLib::array_merge_recursive() can also be of type null; however, SS_HTTPRequest::__construct() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
126
			@file_get_contents('php://input')
127
		);
128
129
		$headers = self::extract_request_headers($_SERVER);
130
		foreach ($headers as $header => $value) {
131
			$req->addHeader($header, $value);
132
		}
133
134
		// Initiate an empty session - doesn't initialize an actual PHP session until saved (see below)
135
		$session = Injector::inst()->create('Session', isset($_SESSION) ? $_SESSION : array());
136
137
		// Only resume a session if its not started already, and a session identifier exists
138
		if(!isset($_SESSION) && Session::request_contains_session_id()) {
139
			$session->inst_start();
140
		}
141
142
		$output = Injector::inst()->get('RequestProcessor')->preRequest($req, $session, $model);
143
144
		if ($output === false) {
145
			// @TODO Need to NOT proceed with the request in an elegant manner
146
			throw new SS_HTTPResponse_Exception(_t('Director.INVALID_REQUEST', 'Invalid request'), 400);
147
		}
148
149
		$result = Director::handleRequest($req, $session, $model);
150
151
		// Save session data. Note that inst_save() will start/resume the session if required.
152
		$session->inst_save();
153
154
		// Return code for a redirection request
155
		if(is_string($result) && substr($result,0,9) == 'redirect:') {
156
			$url = substr($result, 9);
157
158
			if(Director::is_cli()) {
159
				// on cli, follow SilverStripe redirects automatically
160
				return Director::direct(
161
					str_replace(Director::absoluteBaseURL(), '', $url),
162
					DataModel::inst()
163
				);
164
			} else {
165
				$response = new SS_HTTPResponse();
166
				$response->redirect($url);
167
				$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
168
169
				if ($res !== false) {
170
					$response->output();
171
				}
172
			}
173
		// Handle a controller
174
		} else if($result) {
175
			if($result instanceof SS_HTTPResponse) {
176
				$response = $result;
177
178
			} else {
179
				$response = new SS_HTTPResponse();
180
				$response->setBody($result);
181
			}
182
183
			$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
184
			if ($res !== false) {
185
					$response->output();
186
			} else {
187
				// @TODO Proper response here.
188
				throw new SS_HTTPResponse_Exception("Invalid response");
189
			}
190
191
192
			//$controllerObj->getSession()->inst_save();
193
		}
194
	}
195
196
	/**
197
	 * Test a URL request, returning a response object.
198
	 *
199
	 * This method is the counterpart of Director::direct() that is used in functional testing.  It will execute the
200
	 * URL given, and return the result as an SS_HTTPResponse object.
201
	 *
202
	 * @param string $url The URL to visit
203
	 * @param array $postVars The $_POST & $_FILES variables
204
	 * @param Session $session The {@link Session} object representing the current session.  By passing the same
205
	 *                         object to multiple  calls of Director::test(), you can simulate a persisted session.
206
	 * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if postVars is set,
207
	 *                           GET otherwise. Overwritten by $postVars['_method'] if present.
208
	 * @param string $body The HTTP body
209
	 * @param array $headers HTTP headers with key-value pairs
210
	 * @param array|Cookie_Backend $cookies to populate $_COOKIE
211
	 * @param HTTP_Request $request The {@see HTTP_Request} object generated as a part of this request
212
	 * @return SS_HTTPResponse
213
	 *
214
	 * @uses getControllerForURL() The rule-lookup logic is handled by this.
215
	 * @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
216
	 */
217
	public static function test($url, $postVars = null, $session = array(), $httpMethod = null, $body = null,
218
			$headers = array(), $cookies = array(), &$request = null) {
219
220
		Config::nest();
221
		Injector::nest();
222
223
		// These are needed so that calling Director::test() doesnt muck with whoever is calling it.
224
		// Really, it's some inappropriate coupling and should be resolved by making less use of statics
225
		$oldStage = Versioned::current_stage();
226
		$getVars = array();
227
228
		if(!$httpMethod) $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET";
0 ignored issues
show
Bug Best Practice introduced by
The expression $httpMethod of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
229
230
		if(!$session) $session = Injector::inst()->create('Session', array());
231
		$cookieJar = $cookies instanceof Cookie_Backend
232
			? $cookies
233
			: Injector::inst()->createWithArgs('Cookie_Backend', array($cookies ?: array()));
234
235
		// Back up the current values of the superglobals
236
		$existingRequestVars = isset($_REQUEST) ? $_REQUEST : array();
237
		$existingGetVars = isset($_GET) ? $_GET : array();
238
		$existingPostVars = isset($_POST) ? $_POST : array();
239
		$existingSessionVars = isset($_SESSION) ? $_SESSION : array();
240
		$existingCookies = isset($_COOKIE) ? $_COOKIE : array();
241
		$existingServer	= isset($_SERVER) ? $_SERVER : array();
242
243
		$existingRequirementsBackend = Requirements::backend();
244
245
		Config::inst()->update('Cookie', 'report_errors', false);
246
		Requirements::set_backend(new Requirements_Backend());
247
248
		// Set callback to invoke prior to return
249
		$onCleanup = function() use(
250
			$existingRequestVars, $existingGetVars, $existingPostVars, $existingSessionVars,
251
			$existingCookies, $existingServer, $existingRequirementsBackend, $oldStage
252
		) {
253
			// Restore the superglobals
254
			$_REQUEST = $existingRequestVars;
255
			$_GET = $existingGetVars;
256
			$_POST = $existingPostVars;
257
			$_SESSION = $existingSessionVars;
258
			$_COOKIE = $existingCookies;
259
			$_SERVER = $existingServer;
260
261
			Requirements::set_backend($existingRequirementsBackend);
262
263
			// These are needed so that calling Director::test() doesnt muck with whoever is calling it.
264
			// Really, it's some inappropriate coupling and should be resolved by making less use of statics
265
			Versioned::reading_stage($oldStage);
266
267
			Injector::unnest(); // Restore old CookieJar, etc
268
			Config::unnest();
269
		};
270
271 View Code Duplication
		if (strpos($url, '#') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
272
			$url = substr($url, 0, strpos($url, '#'));
273
		}
274
275
		// Handle absolute URLs
276
		if (parse_url($url, PHP_URL_HOST)) {
277
			$bits = parse_url($url);
278
			// If a port is mentioned in the absolute URL, be sure to add that into the
279
			// HTTP host
280
			if(isset($bits['port'])) {
281
				$_SERVER['HTTP_HOST'] = $bits['host'].':'.$bits['port'];
282
			} else {
283
				$_SERVER['HTTP_HOST'] = $bits['host'];
284
			}
285
		}
286
287
		// Ensure URL is properly made relative.
288
		// Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
289
		$url = self::makeRelative($url);
290
291
		$urlWithQuerystring = $url;
292
		if(strpos($url, '?') !== false) {
293
			list($url, $getVarsEncoded) = explode('?', $url, 2);
294
			parse_str($getVarsEncoded, $getVars);
295
		}
296
297
		// Replace the superglobals with appropriate test values
298
		$_REQUEST = ArrayLib::array_merge_recursive((array) $getVars, (array) $postVars);
299
		$_GET = (array) $getVars;
300
		$_POST = (array) $postVars;
301
		$_SESSION = $session ? $session->inst_getAll() : array();
302
		$_COOKIE = $cookieJar->getAll(false);
303
		Injector::inst()->registerService($cookieJar, 'Cookie_Backend');
304
		$_SERVER['REQUEST_URI'] = Director::baseURL() . $urlWithQuerystring;
305
306
		$request = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
0 ignored issues
show
Bug introduced by
It seems like $getVars can also be of type null; however, SS_HTTPRequest::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $postVars defined by parameter $postVars on line 217 can also be of type null; however, SS_HTTPRequest::__construct() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
307
		if($headers) foreach($headers as $k => $v) $request->addHeader($k, $v);
0 ignored issues
show
Bug Best Practice introduced by
The expression $headers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
308
309
		// Pre-request filtering
310
		// @see issue #2517
311
		$model = DataModel::inst();
312
		$output = Injector::inst()->get('RequestProcessor')->preRequest($request, $session, $model);
313
		if ($output === false) {
314
			$onCleanup();
315
			throw new SS_HTTPResponse_Exception(_t('Director.INVALID_REQUEST', 'Invalid request'), 400);
316
		}
317
318
		// TODO: Pass in the DataModel
319
		$result = Director::handleRequest($request, $session, $model);
320
321
		// Ensure that the result is an SS_HTTPResponse object
322
		if(is_string($result)) {
323
			if(substr($result,0,9) == 'redirect:') {
324
				$response = new SS_HTTPResponse();
325
				$response->redirect(substr($result, 9));
326
				$result = $response;
327
			} else {
328
				$result = new SS_HTTPResponse($result);
329
			}
330
		}
331
332
		$output = Injector::inst()->get('RequestProcessor')->postRequest($request, $result, $model);
333
		if ($output === false) {
334
			$onCleanup();
335
			throw new SS_HTTPResponse_Exception("Invalid response");
336
		}
337
338
		// Return valid response
339
		$onCleanup();
340
		return $result;
341
	}
342
343
	/**
344
	 * Handle an HTTP request, defined with a SS_HTTPRequest object.
345
	 *
346
	 * @return SS_HTTPResponse|string
347
	 */
348
	protected static function handleRequest(SS_HTTPRequest $request, Session $session, DataModel $model) {
349
		$rules = Config::inst()->get('Director', 'rules');
350
351
		if(isset($_REQUEST['debug'])) Debug::show($rules);
352
353
		foreach($rules as $pattern => $controllerOptions) {
0 ignored issues
show
Bug introduced by
The expression $rules of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
354
			if(is_string($controllerOptions)) {
355
				if(substr($controllerOptions,0,2) == '->') {
356
					$controllerOptions = array('Redirect' => substr($controllerOptions,2));
357
				} else {
358
					$controllerOptions = array('Controller' => $controllerOptions);
359
				}
360
			}
361
362
			if(($arguments = $request->match($pattern, true)) !== false) {
363
				$request->setRouteParams($controllerOptions);
364
				// controllerOptions provide some default arguments
365
				$arguments = array_merge($controllerOptions, $arguments);
366
367
				// Find the controller name
368
				if(isset($arguments['Controller'])) $controller = $arguments['Controller'];
369
370
				// Pop additional tokens from the tokeniser if necessary
371
				if(isset($controllerOptions['_PopTokeniser'])) {
372
					$request->shift($controllerOptions['_PopTokeniser']);
373
				}
374
375
				// Handle redirections
376
				if(isset($arguments['Redirect'])) {
377
					return "redirect:" . Director::absoluteURL($arguments['Redirect'], true);
378
379
				} else {
380
					Director::$urlParams = $arguments;
381
					$controllerObj = Injector::inst()->create($controller);
0 ignored issues
show
Bug introduced by
The variable $controller does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
382
					$controllerObj->setSession($session);
383
384
					try {
385
						$result = $controllerObj->handleRequest($request, $model);
386
					} catch(SS_HTTPResponse_Exception $responseException) {
387
						$result = $responseException->getResponse();
388
					}
389
					if(!is_object($result) || $result instanceof SS_HTTPResponse) return $result;
390
391
					user_error("Bad result from url " . $request->getURL() . " handled by " .
392
						get_class($controllerObj)." controller: ".get_class($result), E_USER_WARNING);
393
				}
394
			}
395
		}
396
397
		// No URL rules matched, so return a 404 error.
398
		return new SS_HTTPResponse('No URL rule was matched', 404);
399
	}
400
401
	/**
402
	 * Set url parameters (should only be called internally by RequestHandler->handleRequest()).
403
	 *
404
	 * @param $params array
405
	 */
406
	public static function setUrlParams($params) {
407
		Director::$urlParams = $params;
408
	}
409
410
	/**
411
	 * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree object to return,
412
	 * then this will return the current controller.
413
	 *
414
	 * @return SiteTree
415
	 */
416
	public static function get_current_page() {
417
		return self::$current_page ? self::$current_page : Controller::curr();
0 ignored issues
show
Bug Compatibility introduced by
The expression self::$current_page ? se... : \Controller::curr(); of type SiteTree|Controller adds the type Controller to the return on line 417 which is incompatible with the return type documented by Director::get_current_page of type SiteTree.
Loading history...
418
	}
419
420
	/**
421
	 * Set the currently active {@link SiteTree} object that is being used to respond to the request.
422
	 *
423
	 * @param SiteTree $page
424
	 */
425
	public static function set_current_page($page) {
426
		self::$current_page = $page;
427
	}
428
429
	/**
430
	 * Turns the given URL into an absolute URL.
431
	 * By default non-site root relative urls will be evaluated relative to the current request.
432
	 *
433
	 * @param string $url URL To transform to absolute
434
	 * @param bool $relativeToSiteBase Flag indicating if non-site root relative urls should be
435
	 * evaluated relative to the site BaseURL instead of the current url.
436
	 * @return string The fully qualified URL
437
	 */
438
	public static function absoluteURL($url, $relativeToSiteBase = false) {
439
		if(!isset($_SERVER['REQUEST_URI'])) return false;
440
441
		//a url of . or ./ is the same as an empty url
442
		if ($url == '.' || $url == './') {
443
			$url = '';
444
		}
445
446
		if(strpos($url,'/') === false && !$relativeToSiteBase) {
447
			//if there's no URL we want to force a trailing slash on the link
448
			if (!$url) {
449
				$url = '/';
450
			}
451
			$url = Controller::join_links(dirname($_SERVER['REQUEST_URI'] . 'x'), $url);
452
		}
453
454
		if(substr($url,0,4) != "http") {
455
			if(strpos($url, '/') !== 0) $url = Director::baseURL()  . $url;
456
			// Sometimes baseURL() can return a full URL instead of just a path
457
			if(substr($url,0,4) != "http") $url = self::protocolAndHost() . $url;
458
		}
459
460
		return $url;
461
	}
462
463
	/**
464
	 * Returns the part of the URL, 'http://www.mysite.com'.
465
	 *
466
	 * @return boolean|string The domain from the PHP environment. Returns FALSE is this environment variable isn't
467
	 *                        set.
468
	 */
469
	public static function protocolAndHost() {
470
		$alternate = Config::inst()->get('Director', 'alternate_base_url');
471
		if($alternate) {
472
			if(preg_match('/^(http[^:]*:\/\/[^\/]+)(\/|$)/', $alternate, $matches)) {
473
				return $matches[1];
474
			}
475
		}
476
477
		if(isset($_SERVER['HTTP_HOST'])) {
478
			return Director::protocol() . $_SERVER['HTTP_HOST'];
479
		} else {
480
			global $_FILE_TO_URL_MAPPING;
481
			if(Director::is_cli() && isset($_FILE_TO_URL_MAPPING)) $errorSuggestion = '  You probably want to define '.
482
				'an entry in $_FILE_TO_URL_MAPPING that covers "' . Director::baseFolder() . '"';
483
			else if(Director::is_cli()) $errorSuggestion = '  You probably want to define $_FILE_TO_URL_MAPPING in '.
484
				'your _ss_environment.php as instructed on the "sake" page of the doc.silverstripe.com wiki';
485
			else $errorSuggestion = "";
486
487
			user_error("Director::protocolAndHost() lacks sufficient information - HTTP_HOST not set."
488
				. $errorSuggestion, E_USER_WARNING);
489
			return false;
490
491
		}
492
	}
493
494
	/**
495
	 * Return the current protocol that the site is running under.
496
	 *
497
	 * @return string
498
	 */
499
	public static function protocol() {
500
		return (self::is_https()) ? 'https://' : 'http://';
501
	}
502
503
	/**
504
	 * Return whether the site is running as under HTTPS.
505
	 *
506
	 * @return boolean
507
	 */
508
	public static function is_https() {
509
		$return = false;
0 ignored issues
show
Unused Code introduced by
$return is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
510
511
		// See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
512
		// See https://support.microsoft.com/?kbID=307347
513
		$headerOverride = false;
514 View Code Duplication
		if(TRUSTED_PROXY) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
515
			$headers = (defined('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) ? array(SS_TRUSTED_PROXY_PROTOCOL_HEADER) : null;
516
			if(!$headers) {
517
				// Backwards compatible defaults
518
				$headers = array('HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_PROTOCOL', 'HTTP_FRONT_END_HTTPS');
519
			}
520
			foreach($headers as $header) {
521
				$headerCompareVal = ($header === 'HTTP_FRONT_END_HTTPS' ? 'on' : 'https');
522
				if(!empty($_SERVER[$header]) && strtolower($_SERVER[$header]) == $headerCompareVal) {
523
					$headerOverride = true;
524
					break;
525
				}
526
			}
527
		}
528
529
		if ($protocol = Config::inst()->get('Director', 'alternate_protocol')) {
530
			$return = ($protocol == 'https');
531
		} else if($headerOverride) {
532
			$return = true;
533
		} else if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) {
534
			$return = true;
535
		} else if(isset($_SERVER['SSL'])) {
536
			$return = true;
537
		} else {
538
			$return = false;
539
		}
540
541
		return $return;
542
	}
543
544
	/**
545
	 * Returns the root URL for the site.
546
	 *
547
	 * It will be automatically calculated unless it is overridden with
548
	 * {@link setBaseURL()}.
549
	 *
550
	 * @return string
551
	 */
552
	public static function baseURL() {
553
		$alternate = Config::inst()->get('Director', 'alternate_base_url');
554
555
		if($alternate) {
556
			return $alternate;
557
		} else {
558
			$base = BASE_URL;
559
560
			if($base == '/' || $base == '/.' || $base == '\\') {
561
				$baseURL = '/';
562
			} else {
563
				$baseURL = $base . '/';
564
			}
565
566
			if(defined('BASE_SCRIPT_URL')) {
567
				return $baseURL . BASE_SCRIPT_URL;
568
			}
569
570
			return $baseURL;
571
		}
572
	}
573
574
	/**
575
	 * Sets the root URL for the website.
576
	 * If the site isn't accessible from the URL you provide, weird things will happen.
577
	 *
578
	 * @deprecated 4.0 Use the "Director.alternate_base_url" config setting instead
579
	 */
580
	public static function setBaseURL($baseURL) {
581
		Deprecation::notice('4.0', 'Use the "Director.alternate_base_url" config setting instead');
582
		Config::inst()->update('Director', 'alternate_base_url', $baseURL);
583
	}
584
585
	/**
586
	 * Returns the root filesystem folder for the site.
587
	 * It will be automatically calculated unless it is overridden with {@link setBaseFolder()}.
588
	 */
589
	public static function baseFolder() {
590
		$alternate = Config::inst()->get('Director', 'alternate_base_folder');
591
		return ($alternate) ? $alternate : BASE_PATH;
592
	}
593
594
	/**
595
	 * Sets the root folder for the website.
596
	 * If the site isn't accessible from the folder you provide, weird things will happen.
597
	 *
598
	 * @deprecated 4.0 Use the "Director.alternate_base_folder" config setting instead
599
	 */
600
	public static function setBaseFolder($baseFolder) {
601
		Deprecation::notice('4.0', 'Use the "Director.alternate_base_folder" config setting instead');
602
		Config::inst()->update('Director', 'alternate_base_folder', $baseFolder);
603
	}
604
605
	/**
606
	 * Turns an absolute URL or folder into one that's relative to the root of
607
	 * the site. This is useful when turning a URL into a filesystem reference,
608
	 * or vice versa.
609
	 *
610
	 * @param string $url Accepts both a URL or a filesystem path
611
	 * @return string Either a relative URL if the checks succeeded, or the
612
	 * original (possibly absolute) URL.
613
	 */
614
	public static function makeRelative($url) {
615
		// Allow for the accidental inclusion whitespace and // in the URL
616
		$url = trim(preg_replace('#([^:])//#', '\\1/', $url));
617
618
			$base1 = self::absoluteBaseURL();
619
		$baseDomain = substr($base1, strlen(self::protocol()));
620
621
		// Only bother comparing the URL to the absolute version if $url looks like a URL.
622
		if(preg_match('/^https?[^:]*:\/\//',$url,$matches)) {
623
			$urlProtocol = $matches[0];
624
			$urlWithoutProtocol = substr($url, strlen($urlProtocol));
625
626
			// If we are already looking at baseURL, return '' (substr will return false)
627
			if($url == $base1) {
628
				return '';
629
			}
630
			else if(substr($url,0,strlen($base1)) == $base1) {
631
				return substr($url,strlen($base1));
632
			}
633
			else if(substr($base1,-1)=="/" && $url == substr($base1,0,-1)) {
634
			// Convert http://www.mydomain.com/mysitedir to ''
635
				return "";
636
		}
637
638 View Code Duplication
			if(substr($urlWithoutProtocol,0,strlen($baseDomain)) == $baseDomain) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
639
				return substr($urlWithoutProtocol,strlen($baseDomain));
640
			}
641
		}
642
643
		// test for base folder, e.g. /var/www
644
		$base2 = self::baseFolder();
645
		if(substr($url,0,strlen($base2)) == $base2) return substr($url,strlen($base2));
646
647
		// Test for relative base url, e.g. mywebsite/ if the full URL is http://localhost/mywebsite/
648
		$base3 = self::baseURL();
649
		if(substr($url,0,strlen($base3)) == $base3) {
650
			return substr($url,strlen($base3));
651
		}
652
653
		// Test for relative base url, e.g mywebsite/ if the full url is localhost/myswebsite
654 View Code Duplication
		if(substr($url,0,strlen($baseDomain)) == $baseDomain) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
655
			return substr($url, strlen($baseDomain));
656
		}
657
658
		// Nothing matched, fall back to returning the original URL
659
		return $url;
660
	}
661
662
	/**
663
	 * Returns true if a given path is absolute. Works under both *nix and windows
664
	 * systems
665
	 *
666
	 * @param string $path
667
	 * @return bool
668
	 */
669
	public static function is_absolute($path) {
670
		if($path[0] == '/' || $path[0] == '\\') return true;
671
		return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
672
	}
673
674
	/**
675
	 * Checks if a given URL is absolute (e.g. starts with 'http://' etc.).
676
	 * URLs beginning with "//" are treated as absolute, as browsers take this to mean
677
	 * the same protocol as currently being used.
678
	 *
679
	 * Useful to check before redirecting based on a URL from user submissions
680
	 * through $_GET or $_POST, and avoid phishing attacks by redirecting
681
	 * to an attackers server.
682
	 *
683
	 * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
684
	 * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
685
	 *
686
	 * @param string $url
687
	 * @return boolean
688
	 */
689
	public static function is_absolute_url($url) {
690
		// Strip off the query and fragment parts of the URL before checking
691 View Code Duplication
		if(($queryPosition = strpos($url, '?')) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
692
			$url = substr($url, 0, $queryPosition-1);
693
		}
694 View Code Duplication
		if(($hashPosition = strpos($url, '#')) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
695
			$url = substr($url, 0, $hashPosition-1);
696
		}
697
		$colonPosition = strpos($url, ':');
698
		$slashPosition = strpos($url, '/');
699
		return (
700
			// Base check for existence of a host on a compliant URL
701
			parse_url($url, PHP_URL_HOST)
702
			// Check for more than one leading slash without a protocol.
703
				// While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
704
				// and hence a potential security risk. Single leading slashes are not an issue though.
705
			|| preg_match('%^\s*/{2,}%', $url)
706
			|| (
707
				// If a colon is found, check if it's part of a valid scheme definition
708
				// (meaning its not preceded by a slash).
709
				$colonPosition !== FALSE
710
				&& ($slashPosition === FALSE || $colonPosition < $slashPosition)
711
			)
712
		);
713
	}
714
715
	/**
716
	 * Checks if a given URL is relative by checking {@link is_absolute_url()}.
717
	 *
718
	 * @param string $url
719
	 * @return boolean
720
	 */
721
	public static function is_relative_url($url) {
722
		return (!Director::is_absolute_url($url));
723
	}
724
725
	/**
726
	 * Checks if the given URL is belonging to this "site" (not an external link).
727
	 * That's the case if the URL is relative, as defined by {@link is_relative_url()},
728
	 * or if the host matches {@link protocolAndHost()}.
729
	 *
730
	 * Useful to check before redirecting based on a URL from user submissions
731
	 * through $_GET or $_POST, and avoid phishing attacks by redirecting
732
	 * to an attackers server.
733
	 *
734
	 * @param string $url
735
	 * @return boolean
736
	 */
737
	public static function is_site_url($url) {
738
		$urlHost = parse_url($url, PHP_URL_HOST);
739
		$actualHost = parse_url(self::protocolAndHost(), PHP_URL_HOST);
740
		if($urlHost && $actualHost && $urlHost == $actualHost) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlHost of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $actualHost of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
741
			return true;
742
		} else {
743
			return self::is_relative_url($url);
744
		}
745
	}
746
747
	/**
748
	 * Takes a $_SERVER data array and extracts HTTP request headers.
749
	 *
750
	 * @param  array $data
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
751
	 * @return array
752
	 */
753
	public static function extract_request_headers(array $server) {
754
		$headers = array();
755
756
		foreach($server as $key => $value) {
757
			if(substr($key, 0, 5) == 'HTTP_') {
758
				$key = substr($key, 5);
759
				$key = strtolower(str_replace('_', ' ', $key));
760
				$key = str_replace(' ', '-', ucwords($key));
761
				$headers[$key] = $value;
762
			}
763
		}
764
765
		if(isset($server['CONTENT_TYPE'])) $headers['Content-Type'] = $server['CONTENT_TYPE'];
766
		if(isset($server['CONTENT_LENGTH'])) $headers['Content-Length'] = $server['CONTENT_LENGTH'];
767
768
		return $headers;
769
	}
770
771
	/**
772
	 * Given a filesystem reference relative to the site root, return the full file-system path.
773
	 *
774
	 * @param string $file
775
	 * @return string
776
	 */
777
	public static function getAbsFile($file) {
778
		return self::is_absolute($file) ? $file : Director::baseFolder() . '/' . $file;
779
	}
780
781
	/**
782
	 * Returns true if the given file exists.
783
	 * @param $file Filename specified relative to the site root
784
	 */
785
	public static function fileExists($file) {
786
		// replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
787
		$file = preg_replace('/([^\?]*)?.*/','$1',$file);
788
		return file_exists(Director::getAbsFile($file));
789
	}
790
791
	/**
792
	 * Returns the Absolute URL of the site root.
793
	 */
794
	public static function absoluteBaseURL() {
795
		return Director::absoluteURL(Director::baseURL());
796
	}
797
798
	/**
799
	 * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into the URL.
800
	 */
801
	public static function absoluteBaseURLWithAuth() {
802
		$s = "";
0 ignored issues
show
Unused Code introduced by
$s is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
803
		$login = "";
804
805
		if(isset($_SERVER['PHP_AUTH_USER'])) $login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@";
806
807
		return Director::protocol() . $login .  $_SERVER['HTTP_HOST'] . Director::baseURL();
808
	}
809
810
	/**
811
	 * Skip any further processing and immediately respond with a redirect to the passed URL.
812
	 *
813
	 * @param string $destURL - The URL to redirect to
814
	 */
815
	protected static function force_redirect($destURL) {
816
		$response = new SS_HTTPResponse();
817
		$response->redirect($destURL, 301);
818
819
		HTTP::add_cache_headers($response);
820
821
		// TODO: Use an exception - ATM we can be called from _config.php, before Director#handleRequest's try block
822
		$response->output();
823
		die;
824
	}
825
826
	/**
827
	 * Force the site to run on SSL.
828
	 *
829
	 * To use, call from _config.php. For example:
830
	 * <code>
831
	 * if(Director::isLive()) Director::forceSSL();
832
	 * </code>
833
	 *
834
	 * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
835
	 * patterns for matching relative URLs. For example:
836
	 * <code>
837
	 * if(Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
838
	 * </code>
839
	 *
840
	 * If you want certain parts of your site protected under a different domain, you can specify
841
	 * the domain as an argument:
842
	 * <code>
843
	 * if(Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
844
	 * </code>
845
	 *
846
	 * Note that the session data will be lost when moving from HTTP to HTTPS.
847
	 * It is your responsibility to ensure that this won't cause usability problems.
848
	 *
849
	 * CAUTION: This does not respect the site environment mode. You should check this
850
	 * as per the above examples using Director::isLive() or Director::isTest() for example.
851
	 *
852
	 * @param array $patterns Array of regex patterns to match URLs that should be HTTPS
853
	 * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain
854
	 * @return boolean|string String of URL when unit tests running, boolean FALSE if patterns don't match request URI
855
	 */
856
	public static function forceSSL($patterns = null, $secureDomain = null) {
857
		if(!isset($_SERVER['REQUEST_URI'])) return false;
858
859
		$matched = false;
860
861
		if($patterns) {
862
			// Calling from the command-line?
863
			if(!isset($_SERVER['REQUEST_URI'])) return;
864
865
			$relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
0 ignored issues
show
Security Bug introduced by
It seems like \Director::absoluteURL($_SERVER['REQUEST_URI']) targeting Director::absoluteURL() can also be of type false; however, Director::makeRelative() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
866
867
			// protect portions of the site based on the pattern
868
			foreach($patterns as $pattern) {
869
				if(preg_match($pattern, $relativeURL)) {
870
					$matched = true;
871
					break;
872
				}
873
			}
874
		} else {
875
			// protect the entire site
876
			$matched = true;
877
		}
878
879
		if($matched && !self::is_https()) {
880
881
			// if an domain is specified, redirect to that instead of the current domain
882
			if($secureDomain) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $secureDomain of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
883
				$url = 'https://' . $secureDomain . $_SERVER['REQUEST_URI'];
884
			} else {
885
				$url = $_SERVER['REQUEST_URI'];
886
			}
887
888
			$destURL = str_replace('http:', 'https:', Director::absoluteURL($url));
889
890
			// This coupling to SapphireTest is necessary to test the destination URL and to not interfere with tests
891
			if(class_exists('SapphireTest', false) && SapphireTest::is_running_test()) {
892
				return $destURL;
893
			} else {
894
				self::force_redirect($destURL);
895
			}
896
		} else {
897
			return false;
898
		}
899
	}
900
901
	/**
902
	 * Force a redirect to a domain starting with "www."
903
	 */
904
	public static function forceWWW() {
905
		if(!Director::isDev() && !Director::isTest() && strpos($_SERVER['HTTP_HOST'], 'www') !== 0) {
906
			$destURL = str_replace(Director::protocol(), Director::protocol() . 'www.',
907
				Director::absoluteURL($_SERVER['REQUEST_URI']));
908
909
			self::force_redirect($destURL);
910
		}
911
	}
912
913
	/**
914
	 * Checks if the current HTTP-Request is an "Ajax-Request"
915
	 * by checking for a custom header set by jQuery or
916
	 * wether a manually set request-parameter 'ajax' is present.
917
	 *
918
	 * @return boolean
919
	 */
920
	public static function is_ajax() {
921
		if(Controller::has_curr()) {
922
			return Controller::curr()->getRequest()->isAjax();
923
		} else {
924
			return (
925
				isset($_REQUEST['ajax']) ||
926
				(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
927
			);
928
		}
929
	}
930
931
	/**
932
	 * Returns true if this script is being run from the command line rather than the webserver.
933
	 *
934
	 * @return boolean
935
	 */
936
	public static function is_cli() {
937
		return (php_sapi_name() == "cli");
938
	}
939
940
	////////////////////////////////////////////////////////////////////////////////////////////
941
	// Environment type methods
942
	////////////////////////////////////////////////////////////////////////////////////////////
943
944
	/**
945
	 * Set the environment type of the current site.
946
	 *
947
	 * Typically, a SilverStripe site have a number of environments:
948
	 *  - development environments, such a copy on your local machine.
949
	 *  - test sites, such as the one you show the client before going live.
950
	 *  - the live site itself.
951
	 *
952
	 * The behaviour of these environments often varies slightly.  For example, development sites may have errors
953
	 * dumped to the screen, and order confirmation emails might be sent to the developer instead of the client.
954
	 *
955
	 * To help with this, SilverStripe supports the notion of an environment type.  The environment type can be dev,
956
	 * test, or live.
957
	 *
958
	 * You can set it explicitly with Director::set_environment_tpye().  Or you can use
959
	 * {@link Director::$dev_servers} and {@link Director::$test_servers} to set it implicitly, based on the
960
	 * value of $_SERVER['HTTP_HOST'].  If the HTTP_HOST value is one of the servers listed, then the environment type
961
	 * will be test or dev.  Otherwise, the environment type will be live.
962
	 *
963
	 * Dev mode can also be forced by putting ?isDev=1 in your URL, which will ask you to log in and then push the
964
	 * site into dev mode for the remainder of the session. Putting ?isDev=0 onto the URL can turn it back.
965
	 *
966
	 * Test mode can also be forced by putting ?isTest=1 in your URL, which will ask you to log in and then push the
967
	 * site into test mode for the remainder of the session. Putting ?isTest=0 onto the URL can turn it back.
968
	 *
969
	 * Generally speaking, these methods will be called from your _config.php file.
970
	 *
971
	 * Once the environment type is set, it can be checked with {@link Director::isDev()}, {@link Director::isTest()},
972
	 * and {@link Director::isLive()}.
973
	 *
974
	 * @deprecated 4.0 Use the "Director.environment_type" config setting instead
975
	 * @param $et string The environment type: dev, test, or live.
976
	 */
977
	public static function set_environment_type($et) {
978
		if($et != 'dev' && $et != 'test' && $et != 'live') {
979
			user_error("Director::set_environment_type passed '$et'.  It should be passed dev, test, or live",
980
				E_USER_WARNING);
981
		} else {
982
			Deprecation::notice('4.0', 'Use the "Director.environment_type" config setting instead');
983
			Config::inst()->update('Director', 'environment_type', $et);
984
		}
985
	}
986
987
	/**
988
	 * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and {@link Director::isLive()}.
989
	 *
990
	 * @return string 'dev', 'test' or 'live'
991
	 */
992
	public static function get_environment_type() {
993
		if(Director::isLive()) {
994
			return 'live';
995
		} elseif(Director::isTest()) {
996
			return 'test';
997
		} elseif(Director::isDev()) {
998
			return 'dev';
999
		} else {
1000
			return false;
1001
		}
1002
	}
1003
1004
	/*
1005
	 * This function will return true if the site is in a live environment.
1006
	 * For information about environment types, see {@link Director::set_environment_type()}.
1007
	 */
1008
	public static function isLive() {
1009
		return !(Director::isDev() || Director::isTest());
1010
	}
1011
1012
	/**
1013
	 * This function will return true if the site is in a development environment.
1014
	 * For information about environment types, see {@link Director::set_environment_type()}.
1015
	 */
1016 View Code Duplication
	public static function isDev() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1017
		// Check session
1018
		if($env = self::session_environment()) return $env === 'dev';
1019
1020
		// Check config
1021
		if(Config::inst()->get('Director', 'environment_type') === 'dev') return true;
1022
1023
		// Check if we are running on one of the test servers
1024
		$devServers = (array)Config::inst()->get('Director', 'dev_servers');
1025
		if(isset($_SERVER['HTTP_HOST']) && in_array($_SERVER['HTTP_HOST'], $devServers))  {
1026
			return true;
1027
		}
1028
1029
		return false;
1030
	}
1031
1032
	/**
1033
	 * This function will return true if the site is in a test environment.
1034
	 * For information about environment types, see {@link Director::set_environment_type()}.
1035
	 */
1036 View Code Duplication
	public static function isTest() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1037
		// In case of isDev and isTest both being set, dev has higher priority
1038
		if(self::isDev()) return false;
1039
1040
		// Check saved session
1041
		if($env = self::session_environment()) return $env === 'test';
1042
1043
		// Check config
1044
		if(Config::inst()->get('Director', 'environment_type') === 'test') return true;
1045
1046
		// Check if we are running on one of the test servers
1047
		$testServers = (array)Config::inst()->get('Director', 'test_servers');
1048
		if(isset($_SERVER['HTTP_HOST']) && in_array($_SERVER['HTTP_HOST'], $testServers))  {
1049
			return true;
1050
		}
1051
1052
		return false;
1053
	}
1054
1055
	/**
1056
	 * Check or update any temporary environment specified in the session
1057
	 *
1058
	 * @return string 'test', 'dev', or null
1059
	 */
1060
	protected static function session_environment() {
1061
		// Set session from querystring
1062
		if(isset($_GET['isDev'])) {
1063
			if(isset($_SESSION)) {
1064
				unset($_SESSION['isTest']); // In case we are changing from test mode
1065
				$_SESSION['isDev'] = $_GET['isDev'];
1066
			}
1067
			return 'dev';
1068
		} elseif(isset($_GET['isTest'])) {
1069
			if(isset($_SESSION)) {
1070
				unset($_SESSION['isDev']); // In case we are changing from dev mode
1071
				$_SESSION['isTest'] = $_GET['isTest'];
1072
			}
1073
			return 'test';
1074
		}
1075
		// Check session
1076
		if(isset($_SESSION['isDev']) && $_SESSION['isDev']) {
1077
			return 'dev';
1078
		} elseif(isset($_SESSION['isTest']) && $_SESSION['isTest']) {
1079
			return 'test';
1080
		} else {
1081
			return null;
1082
		}
1083
	}
1084
1085
	/**
1086
	 * @return array Returns an array of strings of the method names of methods on the call that should be exposed
1087
	 * as global variables in the templates.
1088
	 */
1089
	public static function get_template_global_variables() {
1090
		return array(
1091
			'absoluteBaseURL',
1092
			'baseURL',
1093
			'is_ajax',
1094
			'isAjax' => 'is_ajax',
1095
			'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1096
		);
1097
	}
1098
}
1099