Completed
Push — director-middleware ( 059969...3cff84 )
by Sam
08:52
created

Director::absoluteURL()   C

Complexity

Conditions 12
Paths 27

Size

Total Lines 40
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 19
nc 27
nop 2
dl 0
loc 40
rs 5.1612
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\Environment;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Core\Kernel;
10
use SilverStripe\Dev\Deprecation;
11
use SilverStripe\Versioned\Versioned;
12
use SilverStripe\View\Requirements;
13
use SilverStripe\View\Requirements_Backend;
14
use SilverStripe\View\TemplateGlobalProvider;
15
16
/**
17
 * Director is responsible for processing URLs, and providing environment information.
18
 *
19
 * The most important part of director is {@link Director::handleRequest()}, which is passed an HTTPRequest and will
20
 * execute the appropriate controller.
21
 *
22
 * Director also has a number of static methods that provide information about the environment, such as
23
 * {@link Director::$environment_type}.
24
 *
25
 * @see Director::handleRequest()
26
 * @see Director::$rules
27
 * @see Director::$environment_type
28
 */
29
class Director implements TemplateGlobalProvider
30
{
31
    use Configurable;
32
33
    /**
34
     * Specifies this url is relative to the base.
35
     *
36
     * @var string
37
     */
38
    const BASE = 'BASE';
39
40
    /**
41
     * Specifies this url is relative to the site root.
42
     *
43
     * @var string
44
     */
45
    const ROOT = 'ROOT';
46
47
    /**
48
     * specifies this url is relative to the current request.
49
     *
50
     * @var string
51
     */
52
    const REQUEST = 'REQUEST';
53
54
    /**
55
     * @config
56
     * @var array
57
     */
58
    private static $rules = array();
59
60
    /**
61
     * Set current page
62
     *
63
     * @internal
64
     * @var SiteTree
65
     */
66
    private static $current_page;
67
68
    /**
69
     * @config
70
     * @var string
71
     */
72
    private static $alternate_base_folder;
73
74
    /**
75
     * Force the base_url to a specific value.
76
     * If assigned, default_base_url and the value in the $_SERVER
77
     * global is ignored.
78
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
79
     *
80
     * @config
81
     * @var string
82
     */
83
    private static $alternate_base_url;
84
85
    /**
86
     * Base url to populate if cannot be determined otherwise.
87
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
88
     *
89
     * @config
90
     * @var string
91
     */
92
    private static $default_base_url = '`SS_BASE_URL`';
93
94
    /**
95
     * Assigned environment type
96
     *
97
     * @internal
98
     * @var string
99
     */
100
    protected static $environment_type;
101
102
    /**
103
     * Test a URL request, returning a response object. This method is a wrapper around
104
     * Director::handleRequest() to assist with functional testing. It will execute the URL given, and
105
     * return the result as an HTTPResponse object.
106
     *
107
     * @param string $url The URL to visit.
108
     * @param array $postVars The $_POST & $_FILES variables.
109
     * @param array|Session $session The {@link Session} object representing the current session.
110
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
111
     * session.
112
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
113
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
114
     * @param string $body The HTTP body.
115
     * @param array $headers HTTP headers with key-value pairs.
116
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
117
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
118
     *
119
     * @return HTTPResponse
120
     *
121
     * @throws HTTPResponse_Exception
122
     */
123
    public static function test(
124
        $url,
125
        $postVars = [],
126
        $session = array(),
127
        $httpMethod = null,
128
        $body = null,
129
        $headers = array(),
130
        $cookies = array(),
131
        &$request = null
132
    ) {
133
        return static::mockRequest(
134
            function (HTTPRequest $request) {
135
                return static::handleRequest($request);
136
            },
137
            $url,
138
            $postVars,
139
            $session,
140
            $httpMethod,
141
            $body,
142
            $headers,
143
            $cookies,
144
            $request
145
        );
146
    }
147
148
    /**
149
     * Mock a request, passing this to the given callback, before resetting.
150
     *
151
     * @param callable $callback Action to pass the HTTPRequst object
152
     * @param string $url The URL to build
153
     * @param array $postVars The $_POST & $_FILES variables.
154
     * @param array|Session $session The {@link Session} object representing the current session.
155
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
156
     * session.
157
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
158
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
159
     * @param string $body The HTTP body.
160
     * @param array $headers HTTP headers with key-value pairs.
161
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
162
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
163
     * @return mixed Result of callback
164
     */
165
    public static function mockRequest(
166
        $callback,
0 ignored issues
show
Unused Code introduced by
The parameter $callback 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...
167
        $url,
168
        $postVars = [],
169
        $session = [],
170
        $httpMethod = null,
171
        $body = null,
172
        $headers = [],
173
        $cookies = [],
174
        &$request = null
175
    ) {
176
        // Build list of cleanup promises
177
        $finally = [];
178
179
        /** @var Kernel $kernel */
180
        $kernel = Injector::inst()->get(Kernel::class);
181
        $kernel->nest();
182
        $finally[] = function () use ($kernel) {
183
            $kernel->activate();
184
        };
185
186
        // backup existing vars, and create new vars
187
        $existingVars = Environment::getVariables();
188
        $finally[] = function () use ($existingVars) {
189
            Environment::setVariables($existingVars);
190
        };
191
        $newVars = $existingVars;
192
193
        // These are needed so that calling Director::test() does not muck with whoever is calling it.
194
        // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
195
        if (class_exists(Versioned::class)) {
196
            $oldReadingMode = Versioned::get_reading_mode();
197
            $finally[] = function () use ($oldReadingMode) {
198
                Versioned::set_reading_mode($oldReadingMode);
199
            };
200
        }
201
202
        // Default httpMethod
203
        $newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod ?: ($postVars ? "POST" : "GET");
204
        $newVars['_POST'] = (array)$postVars;
205
206
        // Setup session
207
        if ($session instanceof Session) {
208
            // Note: If passing $session as object, ensure that changes are written back
209
            // This is important for classes such as FunctionalTest which emulate cross-request persistence
210
            $newVars['_SESSION'] = $session->getAll();
211
            $finally[] = function () use ($session) {
212
                if (isset($_SESSION)) {
213
                    foreach ($_SESSION as $key => $value) {
214
                        $session->set($key, $value);
215
                    }
216
                }
217
            };
218
        } else {
219
            $newVars['_SESSION'] = $session ?: [];
220
        }
221
222
        // Setup cookies
223
        $cookieJar = $cookies instanceof Cookie_Backend
224
            ? $cookies
225
            : Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: []));
226
        $newVars['_COOKIE'] = $cookieJar->getAll(false);
227
        Cookie::config()->update('report_errors', false);
228
        Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
229
230
        // Backup requirements
231
        $existingRequirementsBackend = Requirements::backend();
232
        Requirements::set_backend(Requirements_Backend::create());
233
        $finally[] = function () use ($existingRequirementsBackend) {
234
            Requirements::set_backend($existingRequirementsBackend);
235
        };
236
237
        // Strip any hash
238
        $url = strtok($url, '#');
239
240
        // Handle absolute URLs
241
        if (parse_url($url, PHP_URL_HOST)) {
242
            $bits = parse_url($url);
243
244
            // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
245
            $newVars['_SERVER']['HTTP_HOST'] = isset($bits['port'])
246
                ? $bits['host'].':'.$bits['port']
247
                : $bits['host'];
248
        }
249
250
        // Ensure URL is properly made relative.
251
        // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
252
        $url = self::makeRelative($url);
253
        if (strpos($url, '?') !== false) {
254
            list($url, $getVarsEncoded) = explode('?', $url, 2);
255
            parse_str($getVarsEncoded, $newVars['_GET']);
256
        } else {
257
            $newVars['_GET'] = [];
258
        }
259
        $newVars['_SERVER']['REQUEST_URI'] = Director::baseURL() . $url;
260
        $newVars['_REQUEST'] = array_merge($newVars['_GET'], $newVars['_POST']);
261
262
        // Normalise vars
263
        $newVars = HTTPRequestBuilder::cleanEnvironment($newVars);
264
265
        // Create new request
266
        $request = HTTPRequestBuilder::createFromVariables($newVars, $body);
267
        if ($headers) {
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...
268
            foreach ($headers as $k => $v) {
269
                $request->addHeader($k, $v);
270
            }
271
        }
272
273
        // Apply new vars to environment
274
        Environment::setVariables($newVars);
275
276
        try {
277
            // Normal request handling
278
            return call_user_func($callback, $request);
279
        } finally {
280
            // Restore state in reverse order to assignment
281
            foreach (array_reverse($finally) as $callback) {
282
                call_user_func($callback);
283
            }
284
        }
285
    }
286
287
    /**
288
     * Process the given URL, creating the appropriate controller and executing it.
289
     *
290
     * Request processing is handled as follows:
291
     * - Director::handleRequest($request) checks each of the Director rules and identifies a controller
292
     *   to handle this request.
293
     * - Controller::handleRequest($request) is then called.  This will find a rule to handle the URL,
294
     *   and call the rule handling method.
295
     * - RequestHandler::handleRequest($request) is recursively called whenever a rule handling method
296
     *   returns a RequestHandler object.
297
     *
298
     * In addition to request processing, Director will manage the session, and perform the output of
299
     * the actual response to the browser.
300
     *
301
     * @uses handleRequest() rule-lookup logic is handled by this.
302
     * @param HTTPRequest $request
303
     * @return HTTPResponse
304
     * @throws HTTPResponse_Exception
305
     * @param HTTPRequest $request
306
     * @return HTTPResponse
307
     */
308
    public static function handleRequest(HTTPRequest $request)
309
    {
310
        Injector::inst()->registerService($request, HTTPRequest::class);
311
312
        $rules = Director::config()->uninherited('rules');
313
314
        // Get global middlewares
315
        $middlewares = Director::config()->uninherited('middlewares') ?: [];
316
317
        // Default handler - mo URL rules matched, so return a 404 error.
318
        $handler = function () {
319
            return new HTTPResponse('No URL rule was matched', 404);
320
        };
321
322
        foreach ($rules as $pattern => $controllerOptions) {
323
            // Normalise route rule
324
            if (is_string($controllerOptions)) {
325
                if (substr($controllerOptions, 0, 2) == '->') {
326
                    $controllerOptions = array('Redirect' => substr($controllerOptions, 2));
327
                } else {
328
                    $controllerOptions = array('Controller' => $controllerOptions);
329
                }
330
            }
331
332
            // Add controller-specific middlewares
333
            if (isset($controllerOptions['Middlewares'])) {
334
                // Force to array
335
                if (!is_array($controllerOptions['Middlewares'])) {
336
                    $controllerOptions['Middlewares'] = [$controllerOptions['Middlewares']];
337
                }
338
                $middlewares = array_merge($middlewares, $controllerOptions['Middlewares']);
339
            }
340
341
            // Remove null middlewares (may be included due to limitatons of config yml)
342
            $middlewares = array_filter($middlewares);
343
344
            // Match pattern
345
            $arguments = $request->match($pattern, true);
346
            if ($arguments !== false) {
347
                $request->setRouteParams($controllerOptions);
348
                // controllerOptions provide some default arguments
349
                $arguments = array_merge($controllerOptions, $arguments);
350
351
                // Pop additional tokens from the tokenizer if necessary
352
                if (isset($controllerOptions['_PopTokeniser'])) {
353
                    $request->shift($controllerOptions['_PopTokeniser']);
354
                }
355
356
                // Handler for redirection
357
                if (isset($arguments['Redirect'])) {
358
                    $handler = function () use ($arguments) {
359
                        // Redirection
360
                        $response = new HTTPResponse();
361
                        $response->redirect(static::absoluteURL($arguments['Redirect']));
0 ignored issues
show
Security Bug introduced by
It seems like static::absoluteURL($arguments['Redirect']) targeting SilverStripe\Control\Director::absoluteURL() can also be of type false; however, SilverStripe\Control\HTTPResponse::redirect() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
362
                        return $response;
363
                    };
364
                    break;
365
                }
366
367
                // Find the controller name
368
                $controller = $arguments['Controller'];
369
                $controllerObj = Injector::inst()->create($controller);
370
371
                // Handler for calling a controller
372
                $handler = function ($request) use ($controllerObj) {
373
                    try {
374
                        return $controllerObj->handleRequest($request);
375
                    } catch (HTTPResponse_Exception $responseException) {
376
                        return $responseException->getResponse();
377
                    }
378
                };
379
                break;
380
            }
381
        }
382
383
        // Call the handler with the given middlewares
384
        $response = self::callWithMiddlewares(
385
            $request,
386
            $middlewares,
387
            $handler
388
        );
389
390
        // Note that if a different request was previously registered, this will now be lost
391
        // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
392
        Injector::inst()->unregisterNamedObject(HTTPRequest::class);
393
394
        return $response;
395
    }
396
397
    /**
398
     * Call the given request handler with the given middlewares
399
     * Middlewares are specified as Injector service names
400
     *
401
     * @param $request The request to pass to the handler
402
     * @param $middlewareNames The services names of the middlewares to apply
403
     * @param $handler The request handler
404
     */
405
    protected static function callWithMiddlewares(HTTPRequest $request, array $middlewareNames, callable $handler)
406
    {
407
        $next = $handler;
408
409
        if ($middlewareNames) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $middlewareNames 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...
410
            $middlewares = array_map(
411
                function ($name) {
412
                    return Injector::inst()->get($name);
413
                },
414
                $middlewareNames
415
            );
416
417
            // Reverse middlewares
418
            /** @var HTTPMiddleware $middleware */
419
            foreach (array_reverse($middlewares) as $middleware) {
420
                $next = function ($request) use ($middleware, $next) {
421
                    return $middleware->process($request, $next);
422
                };
423
            }
424
        }
425
426
        return $next($request);
427
    }
428
429
    /**
430
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
431
     * object to return, then this will return the current controller.
432
     *
433
     * @return SiteTree|Controller
434
     */
435
    public static function get_current_page()
436
    {
437
        return self::$current_page ? self::$current_page : Controller::curr();
438
    }
439
440
    /**
441
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
442
     *
443
     * @param SiteTree $page
444
     */
445
    public static function set_current_page($page)
446
    {
447
        self::$current_page = $page;
448
    }
449
450
    /**
451
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
452
     * evaluated relative to the current base_url.
453
     *
454
     * @param string $url URL To transform to absolute.
455
     * @param string $relativeParent Method to use for evaluating relative urls.
456
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
457
     * Defaults to BASE, which is the same behaviour as template url resolution.
458
     * Ignored if the url is absolute or site root.
459
     *
460
     * @return string
461
     */
462
    public static function absoluteURL($url, $relativeParent = self::BASE)
463
    {
464
        if (is_bool($relativeParent)) {
465
            // Deprecate old boolean second parameter
466
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
467
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
468
        }
469
470
        // Check if there is already a protocol given
471
        if (preg_match('/^http(s?):\/\//', $url)) {
472
            return $url;
473
        }
474
475
        // Absolute urls without protocol are added
476
        // E.g. //google.com -> http://google.com
477
        if (strpos($url, '//') === 0) {
478
            return self::protocol() . substr($url, 2);
479
        }
480
481
        // Determine method for mapping the parent to this relative url
482
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
483
            // Root relative urls always should be evaluated relative to the root
484
            $parent = self::protocolAndHost();
485
        } elseif ($relativeParent === self::REQUEST) {
486
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
487
            if (!isset($_SERVER['REQUEST_URI'])) {
488
                return false;
489
            }
490
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
491
        } else {
492
            // Default to respecting site base_url
493
            $parent = self::absoluteBaseURL();
494
        }
495
496
        // Map empty urls to relative slash and join to base
497
        if (empty($url) || $url === '.' || $url === './') {
498
            $url = '/';
499
        }
500
        return Controller::join_links($parent, $url);
501
    }
502
503
    /**
504
     * A helper to determine the current hostname used to access the site.
505
     * The following are used to determine the host (in order)
506
     *  - Director.alternate_base_url (if it contains a domain name)
507
     *  - Trusted proxy headers
508
     *  - HTTP Host header
509
     *  - SS_BASE_URL env var
510
     *  - SERVER_NAME
511
     *  - gethostname()
512
     *
513
     * @return string
514
     */
515
    public static function host()
516
    {
517
        // Check if overridden by alternate_base_url
518 View Code Duplication
        if ($baseURL = self::config()->get('alternate_base_url')) {
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...
519
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
520
            $host = parse_url($baseURL, PHP_URL_HOST);
521
            if ($host) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $host 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...
522
                return $host;
523
            }
524
        }
525
526
        $request = Injector::inst()->get(HTTPRequest::class);
527
        if ($request && $host = $request->getHeader('Host')) {
528
            return $host;
529
        }
530
531
        // Check given header
532
        if (isset($_SERVER['HTTP_HOST'])) {
533
            return $_SERVER['HTTP_HOST'];
534
        }
535
536
        // Check base url
537 View Code Duplication
        if ($baseURL = self::config()->uninherited('default_base_url')) {
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...
538
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
539
            $host = parse_url($baseURL, PHP_URL_HOST);
540
            if ($host) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $host 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...
541
                return $host;
542
            }
543
        }
544
545
        // Fail over to server_name (least reliable)
546
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
547
    }
548
549
    /**
550
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
551
     * variable isn't set.
552
     *
553
     * @return bool|string
554
     */
555
    public static function protocolAndHost()
556
    {
557
        return static::protocol() . static::host();
558
    }
559
560
    /**
561
     * Return the current protocol that the site is running under.
562
     *
563
     * @return string
564
     */
565
    public static function protocol()
566
    {
567
        return (self::is_https()) ? 'https://' : 'http://';
568
    }
569
570
    /**
571
     * Return whether the site is running as under HTTPS.
572
     *
573
     * @return bool
574
     */
575
    public static function is_https()
576
    {
577
        // Check override from alternate_base_url
578 View Code Duplication
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
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...
579
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
580
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
581
            if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol 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...
582
                return $protocol === 'https';
583
            }
584
        }
585
586
        // Check the current request
587
        $request = Injector::inst()->get(HTTPRequest::class);
588
        if ($request && $host = $request->getHeader('Host')) {
589
            return $request->getScheme() === 'https';
590
        }
591
592
        // Check default_base_url
593 View Code Duplication
        if ($baseURL = self::config()->uninherited('default_base_url')) {
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...
594
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
595
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
596
            if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol 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...
597
                return $protocol === 'https';
598
            }
599
        }
600
601
        return false;
602
    }
603
604
    /**
605
     * Return the root-relative url for the baseurl
606
     *
607
     * @return string Root-relative url with trailing slash.
608
     */
609
    public static function baseURL()
610
    {
611
        // Check override base_url
612
        $alternate = self::config()->get('alternate_base_url');
613
        if ($alternate) {
614
            $alternate = Injector::inst()->convertServiceProperty($alternate);
615
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
616
        }
617
618
        // Get env base url
619
        $baseURL = rtrim(BASE_URL, '/') . '/';
620
621
        // Check if BASE_SCRIPT_URL is defined
622
        // e.g. `index.php/`
623
        if (defined('BASE_SCRIPT_URL')) {
624
            return $baseURL . BASE_SCRIPT_URL;
625
        }
626
627
        return $baseURL;
628
    }
629
630
    /**
631
     * Returns the root filesystem folder for the site. It will be automatically calculated unless
632
     * it is overridden with {@link setBaseFolder()}.
633
     *
634
     * @return string
635
     */
636
    public static function baseFolder()
637
    {
638
        $alternate = Director::config()->uninherited('alternate_base_folder');
639
        return ($alternate) ? $alternate : BASE_PATH;
640
    }
641
642
    /**
643
     * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
644
     * when turning a URL into a filesystem reference, or vice versa.
645
     *
646
     * @param string $url Accepts both a URL or a filesystem path.
647
     *
648
     * @return string
649
     */
650
    public static function makeRelative($url)
651
    {
652
        // Allow for the accidental inclusion whitespace and // in the URL
653
        $url = trim(preg_replace('#([^:])//#', '\\1/', $url));
654
655
        $base1 = self::absoluteBaseURL();
656
        $baseDomain = substr($base1, strlen(self::protocol()));
657
658
        // Only bother comparing the URL to the absolute version if $url looks like a URL.
659
        if (preg_match('/^https?[^:]*:\/\//', $url, $matches)) {
660
            $urlProtocol = $matches[0];
661
            $urlWithoutProtocol = substr($url, strlen($urlProtocol));
662
663
            // If we are already looking at baseURL, return '' (substr will return false)
664
            if ($url == $base1) {
665
                return '';
666
            } elseif (substr($url, 0, strlen($base1)) == $base1) {
667
                return substr($url, strlen($base1));
668
            } elseif (substr($base1, -1) == "/" && $url == substr($base1, 0, -1)) {
669
                // Convert http://www.mydomain.com/mysitedir to ''
670
                return "";
671
            }
672
673 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...
674
                return substr($urlWithoutProtocol, strlen($baseDomain));
675
            }
676
        }
677
678
        // test for base folder, e.g. /var/www
679
        $base2 = self::baseFolder();
680
        if (substr($url, 0, strlen($base2)) == $base2) {
681
            return substr($url, strlen($base2));
682
        }
683
684
        // Test for relative base url, e.g. mywebsite/ if the full URL is http://localhost/mywebsite/
685
        $base3 = self::baseURL();
686
        if (substr($url, 0, strlen($base3)) == $base3) {
687
            return substr($url, strlen($base3));
688
        }
689
690
        // Test for relative base url, e.g mywebsite/ if the full url is localhost/myswebsite
691 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...
692
            return substr($url, strlen($baseDomain));
693
        }
694
695
        // Nothing matched, fall back to returning the original URL
696
        return $url;
697
    }
698
699
    /**
700
     * Returns true if a given path is absolute. Works under both *nix and windows systems.
701
     *
702
     * @param string $path
703
     *
704
     * @return bool
705
     */
706
    public static function is_absolute($path)
707
    {
708
        if (empty($path)) {
709
            return false;
710
        }
711
        if ($path[0] == '/' || $path[0] == '\\') {
712
            return true;
713
        }
714
        return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
715
    }
716
717
    /**
718
     * Determine if the url is root relative (i.e. starts with /, but not with //) SilverStripe
719
     * considers root relative urls as a subset of relative urls.
720
     *
721
     * @param string $url
722
     *
723
     * @return bool
724
     */
725
    public static function is_root_relative_url($url)
726
    {
727
        return strpos($url, '/') === 0 && strpos($url, '//') !== 0;
728
    }
729
730
    /**
731
     * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). URLs beginning with "//"
732
     * are treated as absolute, as browsers take this to mean the same protocol as currently being used.
733
     *
734
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
735
     * and avoid phishing attacks by redirecting to an attackers server.
736
     *
737
     * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
738
     * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
739
     *
740
     * @param string $url
741
     *
742
     * @return bool
743
     */
744
    public static function is_absolute_url($url)
745
    {
746
        // Strip off the query and fragment parts of the URL before checking
747
        if (($queryPosition = strpos($url, '?')) !== false) {
748
            $url = substr($url, 0, $queryPosition-1);
749
        }
750 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...
751
            $url = substr($url, 0, $hashPosition-1);
752
        }
753
        $colonPosition = strpos($url, ':');
754
        $slashPosition = strpos($url, '/');
755
        return (
756
            // Base check for existence of a host on a compliant URL
757
            parse_url($url, PHP_URL_HOST)
758
            // Check for more than one leading slash without a protocol.
759
            // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
760
            // and hence a potential security risk. Single leading slashes are not an issue though.
761
            || preg_match('%^\s*/{2,}%', $url)
762
            || (
763
                // If a colon is found, check if it's part of a valid scheme definition
764
                // (meaning its not preceded by a slash).
765
                $colonPosition !== false
766
                && ($slashPosition === false || $colonPosition < $slashPosition)
767
            )
768
        );
769
    }
770
771
    /**
772
     * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()}.
773
     *
774
     * @param string $url
775
     *
776
     * @return bool
777
     */
778
    public static function is_relative_url($url)
779
    {
780
        return !static::is_absolute_url($url);
781
    }
782
783
    /**
784
     * Checks if the given URL is belonging to this "site" (not an external link). That's the case if
785
     * the URL is relative, as defined by {@link is_relative_url()}, or if the host matches
786
     * {@link protocolAndHost()}.
787
     *
788
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
789
     * and avoid phishing attacks by redirecting to an attackers server.
790
     *
791
     * @param string $url
792
     *
793
     * @return bool
794
     */
795
    public static function is_site_url($url)
796
    {
797
        $urlHost = parse_url($url, PHP_URL_HOST);
798
        $actualHost = parse_url(self::protocolAndHost(), PHP_URL_HOST);
799
        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...
800
            return true;
801
        } else {
802
            return self::is_relative_url($url);
803
        }
804
    }
805
806
    /**
807
     * Given a filesystem reference relative to the site root, return the full file-system path.
808
     *
809
     * @param string $file
810
     *
811
     * @return string
812
     */
813
    public static function getAbsFile($file)
814
    {
815
        return self::is_absolute($file) ? $file : Director::baseFolder() . '/' . $file;
816
    }
817
818
    /**
819
     * Returns true if the given file exists. Filename should be relative to the site root.
820
     *
821
     * @param $file
822
     *
823
     * @return bool
824
     */
825
    public static function fileExists($file)
826
    {
827
        // replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
828
        $file = preg_replace('/([^\?]*)?.*/', '$1', $file);
829
        return file_exists(Director::getAbsFile($file));
830
    }
831
832
    /**
833
     * Returns the Absolute URL of the site root.
834
     *
835
     * @return string
836
     */
837
    public static function absoluteBaseURL()
838
    {
839
        return self::absoluteURL(
840
            self::baseURL(),
841
            self::ROOT
842
        );
843
    }
844
845
    /**
846
     * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
847
     * the URL.
848
     *
849
     * @return string
850
     */
851
    public static function absoluteBaseURLWithAuth()
852
    {
853
        $login = "";
854
855
        if (isset($_SERVER['PHP_AUTH_USER'])) {
856
            $login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@";
857
        }
858
859
        return Director::protocol() . $login .  static::host() . Director::baseURL();
860
    }
861
862
    /**
863
     * Skip any further processing and immediately respond with a redirect to the passed URL.
864
     *
865
     * @param string $destURL
866
     * @throws HTTPResponse_Exception
867
     */
868
    protected static function force_redirect($destURL)
869
    {
870
        // Redirect to installer
871
        $response = new HTTPResponse();
872
        $response->redirect($destURL, 301);
873
        HTTP::add_cache_headers($response);
874
        throw new HTTPResponse_Exception($response);
875
    }
876
877
    /**
878
     * Force the site to run on SSL.
879
     *
880
     * To use, call from _config.php. For example:
881
     * <code>
882
     * if (Director::isLive()) Director::forceSSL();
883
     * </code>
884
     *
885
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
886
     * patterns for matching relative URLs. For example:
887
     * <code>
888
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
889
     * </code>
890
     *
891
     * If you want certain parts of your site protected under a different domain, you can specify
892
     * the domain as an argument:
893
     * <code>
894
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
895
     * </code>
896
     *
897
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
898
     * to ensure that this won't cause usability problems.
899
     *
900
     * CAUTION: This does not respect the site environment mode. You should check this
901
     * as per the above examples using Director::isLive() or Director::isTest() for example.
902
     *
903
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
904
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
905
     * @return bool true if already on SSL, false if doesn't match patterns (or cannot redirect)
906
     * @throws HTTPResponse_Exception Throws exception with redirect, if successful
907
     */
908
    public static function forceSSL($patterns = null, $secureDomain = null)
909
    {
910
        // Already on SSL
911
        if (static::is_https()) {
912
            return true;
913
        }
914
915
        // Can't redirect without a url
916
        if (!isset($_SERVER['REQUEST_URI'])) {
917
            return false;
918
        }
919
920
        if ($patterns) {
921
            $matched = false;
922
            $relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
0 ignored issues
show
Security Bug introduced by
It seems like \SilverStripe\Control\Di..._SERVER['REQUEST_URI']) targeting SilverStripe\Control\Director::absoluteURL() can also be of type false; however, SilverStripe\Control\Director::makeRelative() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
923
924
            // protect portions of the site based on the pattern
925
            foreach ($patterns as $pattern) {
926
                if (preg_match($pattern, $relativeURL)) {
927
                    $matched = true;
928
                    break;
929
                }
930
            }
931
            if (!$matched) {
932
                return false;
933
            }
934
        }
935
936
        // if an domain is specified, redirect to that instead of the current domain
937
        if (!$secureDomain) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $secureDomain 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...
938
            $secureDomain = static::host();
939
        }
940
        $url = 'https://' . $secureDomain . $_SERVER['REQUEST_URI'];
941
942
        // Force redirect
943
        self::force_redirect($url);
944
        return true;
945
    }
946
947
    /**
948
     * Force a redirect to a domain starting with "www."
949
     */
950
    public static function forceWWW()
951
    {
952
        if (!Director::isDev() && !Director::isTest() && strpos(static::host(), 'www') !== 0) {
953
            $destURL = str_replace(
954
                Director::protocol(),
955
                Director::protocol() . 'www.',
956
                Director::absoluteURL($_SERVER['REQUEST_URI'])
957
            );
958
959
            self::force_redirect($destURL);
960
        }
961
    }
962
963
    /**
964
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
965
     * jQuery or whether a manually set request-parameter 'ajax' is present.
966
     *
967
     * @return bool
968
     */
969
    public static function is_ajax()
970
    {
971
        if (Controller::has_curr()) {
972
            return Controller::curr()->getRequest()->isAjax();
973
        } else {
974
            return (
975
                isset($_REQUEST['ajax']) ||
976
                (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
977
            );
978
        }
979
    }
980
981
    /**
982
     * Returns true if this script is being run from the command line rather than the web server.
983
     *
984
     * @return bool
985
     */
986
    public static function is_cli()
987
    {
988
        return php_sapi_name() === "cli";
989
    }
990
991
    /**
992
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
993
     * {@link Director::isLive()}.
994
     *
995
     * @return bool
996
     */
997
    public static function get_environment_type()
998
    {
999
        /** @var Kernel $kernel */
1000
        $kernel = Injector::inst()->get(Kernel::class);
1001
        return $kernel->getEnvironment();
1002
    }
1003
1004
    /**
1005
     * This function will return true if the site is in a live environment. For information about
1006
     * environment types, see {@link Director::set_environment_type()}.
1007
     *
1008
     * @return bool
1009
     */
1010
    public static function isLive()
1011
    {
1012
        return self::get_environment_type() === 'live';
1013
    }
1014
1015
    /**
1016
     * This function will return true if the site is in a development environment. For information about
1017
     * environment types, see {@link Director::set_environment_type()}.
1018
     *
1019
     * @return bool
1020
     */
1021
    public static function isDev()
1022
    {
1023
        return self::get_environment_type() === 'dev';
1024
    }
1025
1026
    /**
1027
     * This function will return true if the site is in a test environment. For information about
1028
     * environment types, see {@link Director::set_environment_type()}.
1029
     *
1030
     * @return bool
1031
     */
1032
    public static function isTest()
1033
    {
1034
        return self::get_environment_type() === 'test';
1035
    }
1036
1037
    /**
1038
     * Returns an array of strings of the method names of methods on the call that should be exposed
1039
     * as global variables in the templates.
1040
     *
1041
     * @return array
1042
     */
1043
    public static function get_template_global_variables()
1044
    {
1045
        return array(
1046
            'absoluteBaseURL',
1047
            'baseURL',
1048
            'is_ajax',
1049
            'isAjax' => 'is_ajax',
1050
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1051
        );
1052
    }
1053
}
1054