Passed
Push — master ( 27a2d0...ca40cb )
by Daniel
17:58 queued 10:56
created

Director::parseHost()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTree was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
7
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Environment;
10
use SilverStripe\Core\Extensible;
11
use SilverStripe\Core\Injector\Injectable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Kernel;
14
use SilverStripe\Core\Path;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\Versioned\Versioned;
17
use SilverStripe\View\Requirements;
18
use SilverStripe\View\Requirements_Backend;
19
use SilverStripe\View\TemplateGlobalProvider;
20
21
/**
22
 * Director is responsible for processing URLs, and providing environment information.
23
 *
24
 * The most important part of director is {@link Director::handleRequest()}, which is passed an HTTPRequest and will
25
 * execute the appropriate controller.
26
 *
27
 * @see Director::handleRequest()
28
 * @see Director::$rules
29
 * @skipUpgrade
30
 */
31
class Director implements TemplateGlobalProvider
32
{
33
    use Configurable;
34
    use Extensible;
35
    use Injectable;
36
    use HTTPMiddlewareAware;
37
38
    /**
39
     * Specifies this url is relative to the base.
40
     *
41
     * @var string
42
     */
43
    const BASE = 'BASE';
44
45
    /**
46
     * Specifies this url is relative to the site root.
47
     *
48
     * @var string
49
     */
50
    const ROOT = 'ROOT';
51
52
    /**
53
     * specifies this url is relative to the current request.
54
     *
55
     * @var string
56
     */
57
    const REQUEST = 'REQUEST';
58
59
    /**
60
     * @config
61
     * @var array
62
     */
63
    private static $rules = array();
0 ignored issues
show
introduced by
The private property $rules is not used, and could be removed.
Loading history...
64
65
    /**
66
     * Set current page
67
     *
68
     * @internal
69
     * @var SiteTree
70
     */
71
    private static $current_page;
72
73
    /**
74
     * @config
75
     * @var string
76
     */
77
    private static $alternate_base_folder;
0 ignored issues
show
introduced by
The private property $alternate_base_folder is not used, and could be removed.
Loading history...
78
79
    /**
80
     * Override PUBLIC_DIR. Set to a non-null value to override.
81
     * Setting to an empty string will disable public dir.
82
     *
83
     * @config
84
     * @var bool|null
85
     */
86
    private static $alternate_public_dir = null;
0 ignored issues
show
introduced by
The private property $alternate_public_dir is not used, and could be removed.
Loading history...
87
88
    /**
89
     * Base url to populate if cannot be determined otherwise.
90
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
91
     *
92
     * @config
93
     * @var string
94
     */
95
    private static $default_base_url = '`SS_BASE_URL`';
0 ignored issues
show
introduced by
The private property $default_base_url is not used, and could be removed.
Loading history...
96
97
    public function __construct()
98
    {
99
    }
100
101
    /**
102
     * Test a URL request, returning a response object. This method is a wrapper around
103
     * Director::handleRequest() to assist with functional testing. It will execute the URL given, and
104
     * return the result as an HTTPResponse object.
105
     *
106
     * @param string $url The URL to visit.
107
     * @param array $postVars The $_POST & $_FILES variables.
108
     * @param array|Session $session The {@link Session} object representing the current session.
109
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
110
     * session.
111
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
112
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
113
     * @param string $body The HTTP body.
114
     * @param array $headers HTTP headers with key-value pairs.
115
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
116
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
117
     *
118
     * @return HTTPResponse
119
     *
120
     * @throws HTTPResponse_Exception
121
     */
122
    public static function test(
123
        $url,
124
        $postVars = [],
125
        $session = array(),
126
        $httpMethod = null,
127
        $body = null,
128
        $headers = array(),
129
        $cookies = array(),
130
        &$request = null
131
    ) {
132
        return static::mockRequest(
133
            function (HTTPRequest $request) {
134
                return Director::singleton()->handleRequest($request);
135
            },
136
            $url,
137
            $postVars,
138
            $session,
139
            $httpMethod,
140
            $body,
141
            $headers,
142
            $cookies,
143
            $request
144
        );
145
    }
146
147
    /**
148
     * Mock a request, passing this to the given callback, before resetting.
149
     *
150
     * @param callable $callback Action to pass the HTTPRequst object
151
     * @param string $url The URL to build
152
     * @param array $postVars The $_POST & $_FILES variables.
153
     * @param array|Session $session The {@link Session} object representing the current session.
154
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
155
     * session.
156
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
157
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
158
     * @param string $body The HTTP body.
159
     * @param array $headers HTTP headers with key-value pairs.
160
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
161
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
162
     * @return mixed Result of callback
163
     */
164
    public static function mockRequest(
165
        $callback,
166
        $url,
167
        $postVars = [],
168
        $session = [],
169
        $httpMethod = null,
170
        $body = null,
171
        $headers = [],
172
        $cookies = [],
173
        &$request = null
174
    ) {
175
        // Build list of cleanup promises
176
        $finally = [];
177
178
        /** @var Kernel $kernel */
179
        $kernel = Injector::inst()->get(Kernel::class);
180
        $kernel->nest();
181
        $finally[] = function () use ($kernel) {
182
            $kernel->activate();
183
        };
184
185
        // backup existing vars, and create new vars
186
        $existingVars = Environment::getVariables();
187
        $finally[] = function () use ($existingVars) {
188
            Environment::setVariables($existingVars);
189
        };
190
        $newVars = $existingVars;
191
192
        // These are needed so that calling Director::test() does not muck with whoever is calling it.
193
        // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
194
        if (class_exists(Versioned::class)) {
195
            $oldReadingMode = Versioned::get_reading_mode();
196
            $finally[] = function () use ($oldReadingMode) {
197
                Versioned::set_reading_mode($oldReadingMode);
198
            };
199
        }
200
201
        // Default httpMethod
202
        $newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod ?: ($postVars ? "POST" : "GET");
0 ignored issues
show
introduced by
The condition $postVars can never be true.
Loading history...
203
        $newVars['_POST'] = (array)$postVars;
204
205
        // Setup session
206
        if ($session instanceof Session) {
207
            // Note: If passing $session as object, ensure that changes are written back
208
            // This is important for classes such as FunctionalTest which emulate cross-request persistence
209
            $newVars['_SESSION'] = $session->getAll();
210
            $finally[] = function () use ($session) {
211
                if (isset($_SESSION)) {
212
                    foreach ($_SESSION as $key => $value) {
213
                        $session->set($key, $value);
214
                    }
215
                }
216
            };
217
        } else {
218
            $newVars['_SESSION'] = $session ?: [];
0 ignored issues
show
introduced by
The condition $session can never be true.
Loading history...
219
        }
220
221
        // Setup cookies
222
        $cookieJar = $cookies instanceof Cookie_Backend
223
            ? $cookies
224
            : Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: []));
0 ignored issues
show
introduced by
The condition $cookies can never be true.
Loading history...
225
        $newVars['_COOKIE'] = $cookieJar->getAll(false);
226
        Cookie::config()->update('report_errors', false);
227
        Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
228
229
        // Backup requirements
230
        $existingRequirementsBackend = Requirements::backend();
231
        Requirements::set_backend(Requirements_Backend::create());
232
        $finally[] = function () use ($existingRequirementsBackend) {
233
            Requirements::set_backend($existingRequirementsBackend);
234
        };
235
236
        // Strip any hash
237
        $url = strtok($url, '#');
238
239
        // Handle absolute URLs
240
        // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
241
        $urlHostPort = static::parseHost($url);
242
        if ($urlHostPort) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlHostPort of type null|string 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...
243
            $newVars['_SERVER']['HTTP_HOST'] = $urlHostPort;
244
        }
245
246
        // Ensure URL is properly made relative.
247
        // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
248
        $url = self::makeRelative($url);
249
        if (strpos($url, '?') !== false) {
250
            list($url, $getVarsEncoded) = explode('?', $url, 2);
251
            parse_str($getVarsEncoded, $newVars['_GET']);
252
        } else {
253
            $newVars['_GET'] = [];
254
        }
255
        $newVars['_SERVER']['REQUEST_URI'] = Director::baseURL() . ltrim($url, '/');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
256
        $newVars['_REQUEST'] = array_merge($newVars['_GET'], $newVars['_POST']);
257
258
        // Normalise vars
259
        $newVars = HTTPRequestBuilder::cleanEnvironment($newVars);
260
261
        // Create new request
262
        $request = HTTPRequestBuilder::createFromVariables($newVars, $body, ltrim($url, '/'));
263
        if ($headers) {
0 ignored issues
show
introduced by
The condition $headers can never be true.
Loading history...
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...
264
            foreach ($headers as $k => $v) {
265
                $request->addHeader($k, $v);
266
            }
267
        }
268
269
        // Apply new vars to environment
270
        Environment::setVariables($newVars);
271
272
        try {
273
            // Normal request handling
274
            return call_user_func($callback, $request);
275
        } finally {
276
            // Restore state in reverse order to assignment
277
            foreach (array_reverse($finally) as $callback) {
278
                call_user_func($callback);
279
            }
280
        }
281
    }
282
283
    /**
284
     * Process the given URL, creating the appropriate controller and executing it.
285
     *
286
     * Request processing is handled as follows:
287
     * - Director::handleRequest($request) checks each of the Director rules and identifies a controller
288
     *   to handle this request.
289
     * - Controller::handleRequest($request) is then called.  This will find a rule to handle the URL,
290
     *   and call the rule handling method.
291
     * - RequestHandler::handleRequest($request) is recursively called whenever a rule handling method
292
     *   returns a RequestHandler object.
293
     *
294
     * In addition to request processing, Director will manage the session, and perform the output of
295
     * the actual response to the browser.
296
     *
297
     * @param HTTPRequest $request
298
     * @return HTTPResponse
299
     * @throws HTTPResponse_Exception
300
     */
301
    public function handleRequest(HTTPRequest $request)
302
    {
303
        Injector::inst()->registerService($request, HTTPRequest::class);
304
305
        $rules = Director::config()->uninherited('rules');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
306
307
        $this->extend('updateRules', $rules);
308
309
        // Default handler - mo URL rules matched, so return a 404 error.
310
        $handler = function () {
311
            return new HTTPResponse('No URL rule was matched', 404);
312
        };
313
314
        foreach ($rules as $pattern => $controllerOptions) {
315
            // Match pattern
316
            $arguments = $request->match($pattern, true);
317
            if ($arguments == false) {
318
                continue;
319
            }
320
321
            // Normalise route rule
322
            if (is_string($controllerOptions)) {
323
                if (substr($controllerOptions, 0, 2) == '->') {
324
                    $controllerOptions = array('Redirect' => substr($controllerOptions, 2));
325
                } else {
326
                    $controllerOptions = array('Controller' => $controllerOptions);
327
                }
328
            }
329
            $request->setRouteParams($controllerOptions);
330
331
            // controllerOptions provide some default arguments
332
            $arguments = array_merge($controllerOptions, $arguments);
0 ignored issues
show
Bug introduced by
It seems like $arguments can also be of type true; however, parameter $array2 of array_merge() does only seem to accept null|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

332
            $arguments = array_merge($controllerOptions, /** @scrutinizer ignore-type */ $arguments);
Loading history...
333
334
            // Pop additional tokens from the tokenizer if necessary
335
            if (isset($controllerOptions['_PopTokeniser'])) {
336
                $request->shift($controllerOptions['_PopTokeniser']);
337
            }
338
339
            // Handler for redirection
340
            if (isset($arguments['Redirect'])) {
341
                $handler = function () use ($arguments) {
342
                    // Redirection
343
                    $response = new HTTPResponse();
344
                    $response->redirect(static::absoluteURL($arguments['Redirect']));
345
                    return $response;
346
                };
347
                break;
348
            }
349
350
            /** @var RequestHandler $controllerObj */
351
            $controllerObj = Injector::inst()->create($arguments['Controller']);
352
353
            // Handler for calling a controller
354
            $handler = function (HTTPRequest $request) use ($controllerObj) {
355
                try {
356
                    return $controllerObj->handleRequest($request);
357
                } catch (HTTPResponse_Exception $responseException) {
358
                    return $responseException->getResponse();
359
                }
360
            };
361
            break;
362
        }
363
364
        // Call the handler with the configured middlewares
365
        $response = $this->callMiddleware($request, $handler);
366
367
        // Note that if a different request was previously registered, this will now be lost
368
        // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
369
        Injector::inst()->unregisterNamedObject(HTTPRequest::class);
370
371
        return $response;
372
    }
373
374
    /**
375
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
376
     * object to return, then this will return the current controller.
377
     *
378
     * @return SiteTree|Controller
379
     */
380
    public static function get_current_page()
381
    {
382
        return self::$current_page ? self::$current_page : Controller::curr();
383
    }
384
385
    /**
386
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
387
     *
388
     * @param SiteTree $page
389
     */
390
    public static function set_current_page($page)
391
    {
392
        self::$current_page = $page;
393
    }
394
395
    /**
396
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
397
     * evaluated relative to the current base_url.
398
     *
399
     * @param string $url URL To transform to absolute.
400
     * @param string $relativeParent Method to use for evaluating relative urls.
401
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
402
     * Defaults to BASE, which is the same behaviour as template url resolution.
403
     * Ignored if the url is absolute or site root.
404
     *
405
     * @return string
406
     */
407
    public static function absoluteURL($url, $relativeParent = self::BASE)
408
    {
409
        if (is_bool($relativeParent)) {
0 ignored issues
show
introduced by
The condition is_bool($relativeParent) can never be true.
Loading history...
410
            // Deprecate old boolean second parameter
411
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
412
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
413
        }
414
415
        // Check if there is already a protocol given
416
        if (preg_match('/^http(s?):\/\//', $url)) {
417
            return $url;
418
        }
419
420
        // Absolute urls without protocol are added
421
        // E.g. //google.com -> http://google.com
422
        if (strpos($url, '//') === 0) {
423
            return self::protocol() . substr($url, 2);
424
        }
425
426
        // Determine method for mapping the parent to this relative url
427
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
428
            // Root relative urls always should be evaluated relative to the root
429
            $parent = self::protocolAndHost();
430
        } elseif ($relativeParent === self::REQUEST) {
431
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
432
            if (!isset($_SERVER['REQUEST_URI'])) {
433
                return false;
434
            }
435
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
436
        } else {
437
            // Default to respecting site base_url
438
            $parent = self::absoluteBaseURL();
439
        }
440
441
        // Map empty urls to relative slash and join to base
442
        if (empty($url) || $url === '.' || $url === './') {
443
            $url = '/';
444
        }
445
        return Controller::join_links($parent, $url);
446
    }
447
448
    /**
449
     * Return only host (and optional port) part of a url
450
     *
451
     * @param string $url
452
     * @return string|null Hostname, and optional port, or null if not a valid host
453
     */
454
    protected static function parseHost($url)
455
    {
456
        // Get base hostname
457
        $host = parse_url($url, PHP_URL_HOST);
458
        if (!$host) {
459
            return null;
460
        }
461
462
        // Include port
463
        $port = parse_url($url, PHP_URL_PORT);
464
        if ($port) {
465
            $host .= ':' . $port;
466
        }
467
468
        return $host;
469
    }
470
471
    /**
472
     * A helper to determine the current hostname used to access the site.
473
     * The following are used to determine the host (in order)
474
     *  - Director.alternate_base_url (if it contains a domain name)
475
     *  - Trusted proxy headers
476
     *  - HTTP Host header
477
     *  - SS_BASE_URL env var
478
     *  - SERVER_NAME
479
     *  - gethostname()
480
     *
481
     * @param HTTPRequest $request
482
     * @return string Host name, including port (if present)
483
     */
484
    public static function host(HTTPRequest $request = null)
485
    {
486
        // Check if overridden by alternate_base_url
487
        if ($baseURL = self::config()->get('alternate_base_url')) {
488
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
489
            $host = static::parseHost($baseURL);
0 ignored issues
show
Bug introduced by
It seems like $baseURL can also be of type array<mixed,mixed|array|array<mixed,mixed>> and array; however, parameter $url of SilverStripe\Control\Director::parseHost() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

489
            $host = static::parseHost(/** @scrutinizer ignore-type */ $baseURL);
Loading history...
490
            if ($host) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $host of type null|string 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...
491
                return $host;
492
            }
493
        }
494
495
        $request = static::currentRequest($request);
496
        if ($request && ($host = $request->getHeader('Host'))) {
0 ignored issues
show
introduced by
The condition $request && $host = $request->getHeader('Host') can never be true.
Loading history...
497
            return $host;
498
        }
499
500
        // Check given header
501
        if (isset($_SERVER['HTTP_HOST'])) {
502
            return $_SERVER['HTTP_HOST'];
503
        }
504
505
        // Check base url
506
        if ($baseURL = self::config()->uninherited('default_base_url')) {
507
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
508
            $host = static::parseHost($baseURL);
509
            if ($host) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $host of type null|string 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...
510
                return $host;
511
            }
512
        }
513
514
        // Fail over to server_name (least reliable)
515
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
516
    }
517
518
    /**
519
     * Return port used for the base URL.
520
     * Note, this will be null if not specified, in which case you should assume the default
521
     * port for the current protocol.
522
     *
523
     * @param HTTPRequest $request
524
     * @return int|null
525
     */
526
    public static function port(HTTPRequest $request = null)
527
    {
528
        $host = static::host($request);
529
        return (int)parse_url($host, PHP_URL_PORT) ?: null;
530
    }
531
532
    /**
533
     * Return host name without port
534
     *
535
     * @param HTTPRequest|null $request
536
     * @return string|null
537
     */
538
    public static function hostName(HTTPRequest $request = null)
539
    {
540
        $host = static::host($request);
541
        return parse_url($host, PHP_URL_HOST) ?: null;
542
    }
543
544
    /**
545
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
546
     * variable isn't set.
547
     *
548
     * @param HTTPRequest $request
549
     * @return bool|string
550
     */
551
    public static function protocolAndHost(HTTPRequest $request = null)
552
    {
553
        return static::protocol($request) . static::host($request);
554
    }
555
556
    /**
557
     * Return the current protocol that the site is running under.
558
     *
559
     * @param HTTPRequest $request
560
     * @return string
561
     */
562
    public static function protocol(HTTPRequest $request = null)
563
    {
564
        return (self::is_https($request)) ? 'https://' : 'http://';
565
    }
566
567
    /**
568
     * Return whether the site is running as under HTTPS.
569
     *
570
     * @param HTTPRequest $request
571
     * @return bool
572
     */
573
    public static function is_https(HTTPRequest $request = null)
574
    {
575
        // Check override from alternate_base_url
576
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
577
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
578
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
0 ignored issues
show
Bug introduced by
It seems like $baseURL can also be of type array<mixed,mixed|array|array<mixed,mixed>> and array; however, parameter $url of parse_url() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

578
            $protocol = parse_url(/** @scrutinizer ignore-type */ $baseURL, PHP_URL_SCHEME);
Loading history...
579
            if ($protocol) {
580
                return $protocol === 'https';
581
            }
582
        }
583
584
        // Check the current request
585
        $request = static::currentRequest($request);
586
        if ($request && ($scheme = $request->getScheme())) {
0 ignored issues
show
introduced by
The condition $request && $scheme = $request->getScheme() can never be true.
Loading history...
587
            return $scheme === 'https';
588
        }
589
590
        // Check default_base_url
591
        if ($baseURL = self::config()->uninherited('default_base_url')) {
592
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
593
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
594
            if ($protocol) {
595
                return $protocol === 'https';
596
            }
597
        }
598
599
        return false;
600
    }
601
602
    /**
603
     * Return the root-relative url for the baseurl
604
     *
605
     * @return string Root-relative url with trailing slash.
606
     */
607
    public static function baseURL()
608
    {
609
        // Check override base_url
610
        $alternate = self::config()->get('alternate_base_url');
611
        if ($alternate) {
612
            $alternate = Injector::inst()->convertServiceProperty($alternate);
613
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
0 ignored issues
show
Bug introduced by
It seems like $alternate can also be of type array<mixed,mixed|array|array<mixed,mixed>> and array; however, parameter $url of parse_url() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

613
            return rtrim(parse_url(/** @scrutinizer ignore-type */ $alternate, PHP_URL_PATH), '/') . '/';
Loading history...
614
        }
615
616
        // Get env base url
617
        $baseURL = rtrim(BASE_URL, '/') . '/';
618
619
        // Check if BASE_SCRIPT_URL is defined
620
        // e.g. `index.php/`
621
        if (defined('BASE_SCRIPT_URL')) {
622
            return $baseURL . BASE_SCRIPT_URL;
0 ignored issues
show
Bug introduced by
The constant SilverStripe\Control\BASE_SCRIPT_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
623
        }
624
625
        return $baseURL;
626
    }
627
628
    /**
629
     * Returns the root filesystem folder for the site. It will be automatically calculated unless
630
     * it is overridden with {@link setBaseFolder()}.
631
     *
632
     * @return string
633
     */
634
    public static function baseFolder()
635
    {
636
        $alternate = Director::config()->uninherited('alternate_base_folder');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
637
        return $alternate ?: BASE_PATH;
638
    }
639
640
    /**
641
     * Check if using a seperate public dir, and if so return this directory
642
     * name.
643
     *
644
     * This will be removed in 5.0 and fixed to 'public'
645
     *
646
     * @return string
647
     */
648
    public static function publicDir()
649
    {
650
        $alternate = self::config()->uninherited('alternate_public_dir');
651
        if (isset($alternate)) {
652
            return $alternate;
653
        }
654
        return PUBLIC_DIR;
655
    }
656
657
    /**
658
     * Gets the webroot of the project, which may be a subfolder of {@see baseFolder()}
659
     *
660
     * @return string
661
     */
662
    public static function publicFolder()
663
    {
664
        $folder = self::baseFolder();
665
        $publicDir = self::publicDir();
666
        if ($publicDir) {
667
            return Path::join($folder, $publicDir);
0 ignored issues
show
Bug introduced by
$folder of type string is incompatible with the type array expected by parameter $parts of SilverStripe\Core\Path::join(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

667
            return Path::join(/** @scrutinizer ignore-type */ $folder, $publicDir);
Loading history...
668
        }
669
670
        return $folder;
671
    }
672
673
    /**
674
     * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
675
     * when turning a URL into a filesystem reference, or vice versa.
676
     *
677
     * Note: You should check {@link Director::is_site_url()} if making an untrusted url relative prior
678
     * to calling this function.
679
     *
680
     * @param string $url Accepts both a URL or a filesystem path.
681
     * @return string
682
     */
683
    public static function makeRelative($url)
684
    {
685
        // Allow for the accidental inclusion whitespace and // in the URL
686
        $url = preg_replace('#([^:])//#', '\\1/', trim($url));
687
688
        // If using a real url, remove protocol / hostname / auth / port
689
        if (preg_match('#^(?<protocol>https?:)?//(?<hostpart>[^/]*)(?<url>(/.*)?)$#i', $url, $matches)) {
690
            $url = $matches['url'];
691
        }
692
693
        // Empty case
694
        if (trim($url, '\\/') === '') {
695
            return '';
696
        }
697
698
        // Remove base folder or url
699
        foreach ([self::publicFolder(), self::baseFolder(), self::baseURL()] as $base) {
700
            // Ensure single / doesn't break comparison (unless it would make base empty)
701
            $base = rtrim($base, '\\/') ?: $base;
702
            if (stripos($url, $base) === 0) {
703
                return ltrim(substr($url, strlen($base)), '\\/');
704
            }
705
        }
706
707
        // Nothing matched, fall back to returning the original URL
708
        return $url;
709
    }
710
711
    /**
712
     * Returns true if a given path is absolute. Works under both *nix and windows systems.
713
     *
714
     * @param string $path
715
     *
716
     * @return bool
717
     */
718
    public static function is_absolute($path)
719
    {
720
        if (empty($path)) {
721
            return false;
722
        }
723
        if ($path[0] == '/' || $path[0] == '\\') {
724
            return true;
725
        }
726
        return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
727
    }
728
729
    /**
730
     * Determine if the url is root relative (i.e. starts with /, but not with //) SilverStripe
731
     * considers root relative urls as a subset of relative urls.
732
     *
733
     * @param string $url
734
     *
735
     * @return bool
736
     */
737
    public static function is_root_relative_url($url)
738
    {
739
        return strpos($url, '/') === 0 && strpos($url, '//') !== 0;
740
    }
741
742
    /**
743
     * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). URLs beginning with "//"
744
     * are treated as absolute, as browsers take this to mean the same protocol as currently being used.
745
     *
746
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
747
     * and avoid phishing attacks by redirecting to an attackers server.
748
     *
749
     * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
750
     * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
751
     *
752
     * @param string $url
753
     *
754
     * @return bool
755
     */
756
    public static function is_absolute_url($url)
757
    {
758
        // Strip off the query and fragment parts of the URL before checking
759
        if (($queryPosition = strpos($url, '?')) !== false) {
0 ignored issues
show
introduced by
The condition $queryPosition = strpos($url, '?') !== false can never be false.
Loading history...
760
            $url = substr($url, 0, $queryPosition - 1);
761
        }
762
        if (($hashPosition = strpos($url, '#')) !== false) {
0 ignored issues
show
introduced by
The condition $hashPosition = strpos($url, '#') !== false can never be false.
Loading history...
763
            $url = substr($url, 0, $hashPosition - 1);
764
        }
765
        $colonPosition = strpos($url, ':');
766
        $slashPosition = strpos($url, '/');
767
        return (
768
            // Base check for existence of a host on a compliant URL
769
            parse_url($url, PHP_URL_HOST)
770
            // Check for more than one leading slash without a protocol.
771
            // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
772
            // and hence a potential security risk. Single leading slashes are not an issue though.
773
            || preg_match('%^\s*/{2,}%', $url)
774
            || (
775
                // If a colon is found, check if it's part of a valid scheme definition
776
                // (meaning its not preceded by a slash).
777
                $colonPosition !== false
778
                && ($slashPosition === false || $colonPosition < $slashPosition)
779
            )
780
        );
781
    }
782
783
    /**
784
     * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()}.
785
     *
786
     * @param string $url
787
     *
788
     * @return bool
789
     */
790
    public static function is_relative_url($url)
791
    {
792
        return !static::is_absolute_url($url);
793
    }
794
795
    /**
796
     * Checks if the given URL is belonging to this "site" (not an external link). That's the case if
797
     * the URL is relative, as defined by {@link is_relative_url()}, or if the host matches
798
     * {@link protocolAndHost()}.
799
     *
800
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
801
     * and avoid phishing attacks by redirecting to an attackers server.
802
     *
803
     * @param string $url
804
     *
805
     * @return bool
806
     */
807
    public static function is_site_url($url)
808
    {
809
        // Validate host[:port]
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
810
        $urlHost = static::parseHost($url);
811
        if ($urlHost && $urlHost === static::host()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlHost of type null|string 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...
812
            return true;
813
        }
814
815
        // Relative urls always are site urls
816
        return self::is_relative_url($url);
817
    }
818
819
    /**
820
     * Given a filesystem reference relative to the site root, return the full file-system path.
821
     *
822
     * @param string $file
823
     *
824
     * @return string
825
     */
826
    public static function getAbsFile($file)
827
    {
828
        // If already absolute
829
        if (self::is_absolute($file)) {
830
            return $file;
831
        }
832
833
        // If path is relative to public folder search there first
834
        if (self::publicDir()) {
835
            $path = Path::join(self::publicFolder(), $file);
0 ignored issues
show
Bug introduced by
self::publicFolder() of type string is incompatible with the type array expected by parameter $parts of SilverStripe\Core\Path::join(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

835
            $path = Path::join(/** @scrutinizer ignore-type */ self::publicFolder(), $file);
Loading history...
836
            if (file_exists($path)) {
837
                return $path;
838
            }
839
        }
840
841
        // Default to base folder
842
        return Path::join(self::baseFolder(), $file);
843
    }
844
845
    /**
846
     * Returns true if the given file exists. Filename should be relative to the site root.
847
     *
848
     * @param $file
849
     *
850
     * @return bool
851
     */
852
    public static function fileExists($file)
853
    {
854
        // replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
855
        $file = preg_replace('/([^\?]*)?.*/', '$1', $file);
856
        return file_exists(Director::getAbsFile($file));
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
857
    }
858
859
    /**
860
     * Returns the Absolute URL of the site root.
861
     *
862
     * @return string
863
     */
864
    public static function absoluteBaseURL()
865
    {
866
        return self::absoluteURL(
867
            self::baseURL(),
868
            self::ROOT
869
        );
870
    }
871
872
    /**
873
     * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
874
     * the URL.
875
     *
876
     * @param HTTPRequest|null $request
877
     * @return string
878
     */
879
    public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
880
    {
881
        // Detect basic auth
882
        $user = $request->getHeader('PHP_AUTH_USER');
0 ignored issues
show
Bug introduced by
The method getHeader() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

882
        /** @scrutinizer ignore-call */ 
883
        $user = $request->getHeader('PHP_AUTH_USER');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
883
        if ($user) {
884
            $password = $request->getHeader('PHP_AUTH_PW');
885
            $login = sprintf("%s:%s@", $user, $password) ;
886
        } else {
887
            $login = '';
888
        }
889
890
        return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
891
    }
892
893
    /**
894
     * Skip any further processing and immediately respond with a redirect to the passed URL.
895
     *
896
     * @param string $destURL
897
     * @throws HTTPResponse_Exception
898
     */
899
    protected static function force_redirect($destURL)
900
    {
901
        // Redirect to installer
902
        $response = new HTTPResponse();
903
        $response->redirect($destURL, 301);
904
        HTTP::add_cache_headers($response);
905
        throw new HTTPResponse_Exception($response);
906
    }
907
908
    /**
909
     * Force the site to run on SSL.
910
     *
911
     * To use, call from the init() method of your PageController. For example:
912
     * <code>
913
     * if (Director::isLive()) Director::forceSSL();
914
     * </code>
915
     *
916
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
917
     * patterns for matching relative URLs. For example:
918
     * <code>
919
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
920
     * </code>
921
     *
922
     * If you want certain parts of your site protected under a different domain, you can specify
923
     * the domain as an argument:
924
     * <code>
925
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
926
     * </code>
927
     *
928
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
929
     * to ensure that this won't cause usability problems.
930
     *
931
     * CAUTION: This does not respect the site environment mode. You should check this
932
     * as per the above examples using Director::isLive() or Director::isTest() for example.
933
     *
934
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
935
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
936
     * Can include port number.
937
     * @param HTTPRequest|null $request Request object to check
938
     */
939
    public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
940
    {
941
        $handler = CanonicalURLMiddleware::singleton()->setForceSSL(true);
942
        if ($patterns) {
943
            $handler->setForceSSLPatterns($patterns);
944
        }
945
        if ($secureDomain) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $secureDomain of type null|string 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...
946
            $handler->setForceSSLDomain($secureDomain);
947
        }
948
        $handler->throwRedirectIfNeeded($request);
949
    }
950
951
    /**
952
     * Force a redirect to a domain starting with "www."
953
     *
954
     * @param HTTPRequest $request
955
     */
956
    public static function forceWWW(HTTPRequest $request = null)
957
    {
958
        $handler = CanonicalURLMiddleware::singleton()->setForceWWW(true);
959
        $handler->throwRedirectIfNeeded($request);
960
    }
961
962
    /**
963
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
964
     * jQuery or whether a manually set request-parameter 'ajax' is present.
965
     *
966
     * @param HTTPRequest $request
967
     * @return bool
968
     */
969
    public static function is_ajax(HTTPRequest $request = null)
970
    {
971
        $request = self::currentRequest($request);
972
        if ($request) {
0 ignored issues
show
introduced by
The condition $request can never be true.
Loading history...
973
            return $request->isAjax();
974
        } else {
975
            return (
976
                isset($_REQUEST['ajax']) ||
977
                (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
978
            );
979
        }
980
    }
981
982
    /**
983
     * Returns true if this script is being run from the command line rather than the web server.
984
     *
985
     * @return bool
986
     */
987
    public static function is_cli()
988
    {
989
        return in_array(php_sapi_name(), ['cli', 'phpdbg']);
990
    }
991
992
    /**
993
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
994
     * {@link Director::isLive()}.
995
     *
996
     * @return string
997
     */
998
    public static function get_environment_type()
999
    {
1000
        /** @var Kernel $kernel */
1001
        $kernel = Injector::inst()->get(Kernel::class);
1002
        return $kernel->getEnvironment();
1003
    }
1004
1005
    /**
1006
     * This function will return true if the site is in a live environment. For information about
1007
     * environment types, see {@link Director::set_environment_type()}.
1008
     *
1009
     * @return bool
1010
     */
1011
    public static function isLive()
1012
    {
1013
        return self::get_environment_type() === 'live';
1014
    }
1015
1016
    /**
1017
     * This function will return true if the site is in a development environment. For information about
1018
     * environment types, see {@link Director::set_environment_type()}.
1019
     *
1020
     * @return bool
1021
     */
1022
    public static function isDev()
1023
    {
1024
        return self::get_environment_type() === 'dev';
1025
    }
1026
1027
    /**
1028
     * This function will return true if the site is in a test environment. For information about
1029
     * environment types, see {@link Director::set_environment_type()}.
1030
     *
1031
     * @return bool
1032
     */
1033
    public static function isTest()
1034
    {
1035
        return self::get_environment_type() === 'test';
1036
    }
1037
1038
    /**
1039
     * Returns an array of strings of the method names of methods on the call that should be exposed
1040
     * as global variables in the templates.
1041
     *
1042
     * @return array
1043
     */
1044
    public static function get_template_global_variables()
1045
    {
1046
        return array(
1047
            'absoluteBaseURL',
1048
            'baseURL',
1049
            'is_ajax',
1050
            'isAjax' => 'is_ajax',
1051
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1052
        );
1053
    }
1054
1055
    /**
1056
     * Helper to validate or check the current request object
1057
     *
1058
     * @param HTTPRequest $request
1059
     * @return HTTPRequest Request object if one is both current and valid
1060
     */
1061
    protected static function currentRequest(HTTPRequest $request = null)
1062
    {
1063
        // Ensure we only use a registered HTTPRequest and don't
1064
        // incidentally construct a singleton
1065
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
1066
            $request = Injector::inst()->get(HTTPRequest::class);
1067
        }
1068
        return $request;
1069
    }
1070
}
1071