Passed
Pull Request — 4 (#8843)
by Daniel
08:11
created

Director::is_site_url()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 11
nc 4
nop 1
dl 0
loc 22
rs 8.4444
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\Dev\Deprecation;
15
use SilverStripe\Versioned\Versioned;
16
use SilverStripe\View\Requirements;
17
use SilverStripe\View\Requirements_Backend;
18
use SilverStripe\View\TemplateGlobalProvider;
19
20
/**
21
 * Director is responsible for processing URLs, and providing environment information.
22
 *
23
 * The most important part of director is {@link Director::handleRequest()}, which is passed an HTTPRequest and will
24
 * execute the appropriate controller.
25
 *
26
 * @see Director::handleRequest()
27
 * @see Director::$rules
28
 * @skipUpgrade
29
 */
30
class Director implements TemplateGlobalProvider
31
{
32
    use Configurable;
33
    use Extensible;
34
    use Injectable;
35
    use HTTPMiddlewareAware;
36
37
    /**
38
     * Specifies this url is relative to the base.
39
     *
40
     * @var string
41
     */
42
    const BASE = 'BASE';
43
44
    /**
45
     * Specifies this url is relative to the site root.
46
     *
47
     * @var string
48
     */
49
    const ROOT = 'ROOT';
50
51
    /**
52
     * specifies this url is relative to the current request.
53
     *
54
     * @var string
55
     */
56
    const REQUEST = 'REQUEST';
57
58
    /**
59
     * @config
60
     * @var array
61
     */
62
    private static $rules = array();
0 ignored issues
show
introduced by
The private property $rules is not used, and could be removed.
Loading history...
63
64
    /**
65
     * Set current page
66
     *
67
     * @internal
68
     * @var SiteTree
69
     */
70
    private static $current_page;
71
72
    /**
73
     * @config
74
     * @var string
75
     */
76
    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...
77
78
    /**
79
     * Force the base_url to a specific value.
80
     * If assigned, default_base_url and the value in the $_SERVER
81
     * global is ignored.
82
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
83
     *
84
     * @config
85
     * @var string
86
     */
87
    private static $alternate_base_url;
0 ignored issues
show
introduced by
The private property $alternate_base_url is not used, and could be removed.
Loading history...
88
89
    /**
90
     * Base url to populate if cannot be determined otherwise.
91
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
92
     *
93
     * @config
94
     * @var string
95
     */
96
    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...
97
98
    public function __construct()
99
    {
100
    }
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 Director::singleton()->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,
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'] = $sessionArray = $session->getAll();
211
            $finally[] = function () use ($session, $sessionArray) {
212
                if (isset($_SESSION)) {
213
                    // Set new / updated keys
214
                    foreach ($_SESSION as $key => $value) {
215
                        $session->set($key, $value);
216
                    }
217
                    // Unset removed keys
218
                    foreach (array_diff_key($sessionArray, $_SESSION) as $key => $value) {
0 ignored issues
show
Bug introduced by
It seems like $sessionArray can also be of type null; however, parameter $array1 of array_diff_key() does only seem to accept 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

218
                    foreach (array_diff_key(/** @scrutinizer ignore-type */ $sessionArray, $_SESSION) as $key => $value) {
Loading history...
219
                        $session->clear($key);
220
                    }
221
                }
222
            };
223
        } else {
224
            $newVars['_SESSION'] = $session ?: [];
225
        }
226
227
        // Setup cookies
228
        $cookieJar = $cookies instanceof Cookie_Backend
229
            ? $cookies
230
            : Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: []));
231
        $newVars['_COOKIE'] = $cookieJar->getAll(false);
232
        Cookie::config()->update('report_errors', false);
233
        Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
234
235
        // Backup requirements
236
        $existingRequirementsBackend = Requirements::backend();
237
        Requirements::set_backend(Requirements_Backend::create());
238
        $finally[] = function () use ($existingRequirementsBackend) {
239
            Requirements::set_backend($existingRequirementsBackend);
240
        };
241
242
        // Strip any hash
243
        $url = strtok($url, '#');
244
245
        // Handle absolute URLs
246
        if (parse_url($url, PHP_URL_HOST)) {
247
            $bits = parse_url($url);
248
249
            // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
250
            $newVars['_SERVER']['HTTP_HOST'] = isset($bits['port'])
251
                ? $bits['host'] . ':' . $bits['port']
252
                : $bits['host'];
253
        }
254
255
        // Ensure URL is properly made relative.
256
        // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
257
        $url = self::makeRelative($url);
258
        if (strpos($url, '?') !== false) {
259
            list($url, $getVarsEncoded) = explode('?', $url, 2);
260
            parse_str($getVarsEncoded, $newVars['_GET']);
261
        } else {
262
            $newVars['_GET'] = [];
263
        }
264
        $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...
265
        $newVars['_REQUEST'] = array_merge($newVars['_GET'], $newVars['_POST']);
266
267
        // Normalise vars
268
        $newVars = HTTPRequestBuilder::cleanEnvironment($newVars);
269
270
        // Create new request
271
        $request = HTTPRequestBuilder::createFromVariables($newVars, $body);
272
        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...
273
            foreach ($headers as $k => $v) {
274
                $request->addHeader($k, $v);
275
            }
276
        }
277
278
        // Apply new vars to environment
279
        Environment::setVariables($newVars);
280
281
        try {
282
            // Normal request handling
283
            return call_user_func($callback, $request);
284
        } finally {
285
            // Restore state in reverse order to assignment
286
            foreach (array_reverse($finally) as $callback) {
0 ignored issues
show
introduced by
$callback is overwriting one of the parameters of this function.
Loading history...
287
                call_user_func($callback);
288
            }
289
        }
290
    }
291
292
    /**
293
     * Process the given URL, creating the appropriate controller and executing it.
294
     *
295
     * Request processing is handled as follows:
296
     * - Director::handleRequest($request) checks each of the Director rules and identifies a controller
297
     *   to handle this request.
298
     * - Controller::handleRequest($request) is then called.  This will find a rule to handle the URL,
299
     *   and call the rule handling method.
300
     * - RequestHandler::handleRequest($request) is recursively called whenever a rule handling method
301
     *   returns a RequestHandler object.
302
     *
303
     * In addition to request processing, Director will manage the session, and perform the output of
304
     * the actual response to the browser.
305
     *
306
     * @param HTTPRequest $request
307
     * @return HTTPResponse
308
     * @throws HTTPResponse_Exception
309
     */
310
    public function handleRequest(HTTPRequest $request)
311
    {
312
        Injector::inst()->registerService($request, HTTPRequest::class);
313
314
        $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...
315
316
        $this->extend('updateRules', $rules);
317
318
        // Default handler - mo URL rules matched, so return a 404 error.
319
        $handler = function () {
320
            return new HTTPResponse('No URL rule was matched', 404);
321
        };
322
323
        foreach ($rules as $pattern => $controllerOptions) {
324
            // Match pattern
325
            $arguments = $request->match($pattern, true);
326
            if ($arguments == false) {
327
                continue;
328
            }
329
330
            // Normalise route rule
331
            if (is_string($controllerOptions)) {
332
                if (substr($controllerOptions, 0, 2) == '->') {
333
                    $controllerOptions = array('Redirect' => substr($controllerOptions, 2));
334
                } else {
335
                    $controllerOptions = array('Controller' => $controllerOptions);
336
                }
337
            }
338
            $request->setRouteParams($controllerOptions);
339
340
            // controllerOptions provide some default arguments
341
            $arguments = array_merge($controllerOptions, $arguments);
342
343
            // Pop additional tokens from the tokenizer if necessary
344
            if (isset($controllerOptions['_PopTokeniser'])) {
345
                $request->shift($controllerOptions['_PopTokeniser']);
346
            }
347
348
            // Handler for redirection
349
            if (isset($arguments['Redirect'])) {
350
                $handler = function () use ($arguments) {
351
                    // Redirection
352
                    $response = new HTTPResponse();
353
                    $response->redirect(static::absoluteURL($arguments['Redirect']));
354
                    return $response;
355
                };
356
                break;
357
            }
358
359
            /** @var RequestHandler $controllerObj */
360
            $controllerObj = Injector::inst()->create($arguments['Controller']);
361
362
            // Handler for calling a controller
363
            $handler = function (HTTPRequest $request) use ($controllerObj) {
364
                try {
365
                    return $controllerObj->handleRequest($request);
366
                } catch (HTTPResponse_Exception $responseException) {
367
                    return $responseException->getResponse();
368
                }
369
            };
370
            break;
371
        }
372
373
        // Call the handler with the configured middlewares
374
        $response = $this->callMiddleware($request, $handler);
375
376
        // Note that if a different request was previously registered, this will now be lost
377
        // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
378
        Injector::inst()->unregisterNamedObject(HTTPRequest::class);
379
380
        return $response;
381
    }
382
383
    /**
384
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
385
     * object to return, then this will return the current controller.
386
     *
387
     * @return SiteTree|Controller
388
     */
389
    public static function get_current_page()
390
    {
391
        return self::$current_page ? self::$current_page : Controller::curr();
392
    }
393
394
    /**
395
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
396
     *
397
     * @param SiteTree $page
398
     */
399
    public static function set_current_page($page)
400
    {
401
        self::$current_page = $page;
402
    }
403
404
    /**
405
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
406
     * evaluated relative to the current base_url.
407
     *
408
     * @param string $url URL To transform to absolute.
409
     * @param string $relativeParent Method to use for evaluating relative urls.
410
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
411
     * Defaults to BASE, which is the same behaviour as template url resolution.
412
     * Ignored if the url is absolute or site root.
413
     *
414
     * @return string
415
     */
416
    public static function absoluteURL($url, $relativeParent = self::BASE)
417
    {
418
        if (is_bool($relativeParent)) {
0 ignored issues
show
introduced by
The condition is_bool($relativeParent) is always false.
Loading history...
419
            // Deprecate old boolean second parameter
420
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
421
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
422
        }
423
424
        // Check if there is already a protocol given
425
        if (preg_match('/^http(s?):\/\//', $url)) {
426
            return $url;
427
        }
428
429
        // Absolute urls without protocol are added
430
        // E.g. //google.com -> http://google.com
431
        if (strpos($url, '//') === 0) {
432
            return self::protocol() . substr($url, 2);
433
        }
434
435
        // Determine method for mapping the parent to this relative url
436
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
437
            // Root relative urls always should be evaluated relative to the root
438
            $parent = self::protocolAndHost();
439
        } elseif ($relativeParent === self::REQUEST) {
440
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
441
            if (!isset($_SERVER['REQUEST_URI'])) {
442
                return false;
443
            }
444
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
445
        } else {
446
            // Default to respecting site base_url
447
            $parent = self::absoluteBaseURL();
448
        }
449
450
        // Map empty urls to relative slash and join to base
451
        if (empty($url) || $url === '.' || $url === './') {
452
            $url = '/';
453
        }
454
        return Controller::join_links($parent, $url);
455
    }
456
457
    /**
458
     * A helper to determine the current hostname used to access the site.
459
     * The following are used to determine the host (in order)
460
     *  - Director.alternate_base_url (if it contains a domain name)
461
     *  - Trusted proxy headers
462
     *  - HTTP Host header
463
     *  - SS_BASE_URL env var
464
     *  - SERVER_NAME
465
     *  - gethostname()
466
     *
467
     * @param HTTPRequest $request
468
     * @return string
469
     */
470
    public static function host(HTTPRequest $request = null)
471
    {
472
        // Check if overridden by alternate_base_url
473
        if ($baseURL = self::config()->get('alternate_base_url')) {
474
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
475
            $host = parse_url($baseURL, PHP_URL_HOST);
0 ignored issues
show
Bug introduced by
It seems like $baseURL can also be of type 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

475
            $host = parse_url(/** @scrutinizer ignore-type */ $baseURL, PHP_URL_HOST);
Loading history...
476
            if ($host) {
477
                return $host;
478
            }
479
        }
480
481
        $request = static::currentRequest($request);
482
        if ($request && ($host = $request->getHeader('Host'))) {
483
            return $host;
484
        }
485
486
        // Check given header
487
        if (isset($_SERVER['HTTP_HOST'])) {
488
            return $_SERVER['HTTP_HOST'];
489
        }
490
491
        // Check base url
492
        if ($baseURL = self::config()->uninherited('default_base_url')) {
493
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
494
            $host = parse_url($baseURL, PHP_URL_HOST);
495
            if ($host) {
496
                return $host;
497
            }
498
        }
499
500
        // Fail over to server_name (least reliable)
501
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
502
    }
503
504
    /**
505
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
506
     * variable isn't set.
507
     *
508
     * @param HTTPRequest $request
509
     * @return bool|string
510
     */
511
    public static function protocolAndHost(HTTPRequest $request = null)
512
    {
513
        return static::protocol($request) . static::host($request);
514
    }
515
516
    /**
517
     * Return the current protocol that the site is running under.
518
     *
519
     * @param HTTPRequest $request
520
     * @return string
521
     */
522
    public static function protocol(HTTPRequest $request = null)
523
    {
524
        return (self::is_https($request)) ? 'https://' : 'http://';
525
    }
526
527
    /**
528
     * Return whether the site is running as under HTTPS.
529
     *
530
     * @param HTTPRequest $request
531
     * @return bool
532
     */
533
    public static function is_https(HTTPRequest $request = null)
534
    {
535
        // Check override from alternate_base_url
536
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
537
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
538
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
0 ignored issues
show
Bug introduced by
It seems like $baseURL can also be of type 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

538
            $protocol = parse_url(/** @scrutinizer ignore-type */ $baseURL, PHP_URL_SCHEME);
Loading history...
539
            if ($protocol) {
540
                return $protocol === 'https';
541
            }
542
        }
543
544
        // Check the current request
545
        $request = static::currentRequest($request);
546
        if ($request && ($scheme = $request->getScheme())) {
547
            return $scheme === 'https';
548
        }
549
550
        // Check default_base_url
551
        if ($baseURL = self::config()->uninherited('default_base_url')) {
552
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
553
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
554
            if ($protocol) {
555
                return $protocol === 'https';
556
            }
557
        }
558
559
        return false;
560
    }
561
562
    /**
563
     * Return the root-relative url for the baseurl
564
     *
565
     * @return string Root-relative url with trailing slash.
566
     */
567
    public static function baseURL()
568
    {
569
        // Check override base_url
570
        $alternate = self::config()->get('alternate_base_url');
571
        if ($alternate) {
572
            $alternate = Injector::inst()->convertServiceProperty($alternate);
573
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
574
        }
575
576
        // Get env base url
577
        $baseURL = rtrim(BASE_URL, '/') . '/';
578
579
        // Check if BASE_SCRIPT_URL is defined
580
        // e.g. `index.php/`
581
        if (defined('BASE_SCRIPT_URL')) {
582
            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...
583
        }
584
585
        return $baseURL;
586
    }
587
588
    /**
589
     * Returns the root filesystem folder for the site. It will be automatically calculated unless
590
     * it is overridden with {@link setBaseFolder()}.
591
     *
592
     * @return string
593
     */
594
    public static function baseFolder()
595
    {
596
        $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...
597
        return ($alternate) ? $alternate : BASE_PATH;
598
    }
599
600
    /**
601
     * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
602
     * when turning a URL into a filesystem reference, or vice versa.
603
     *
604
     * Note: You should check {@link Director::is_site_url()} if making an untrusted url relative prior
605
     * to calling this function.
606
     *
607
     * @param string $url Accepts both a URL or a filesystem path.
608
     * @return string
609
     */
610
    public static function makeRelative($url)
611
    {
612
        // Allow for the accidental inclusion whitespace and // in the URL
613
        $url = preg_replace('#([^:])//#', '\\1/', trim($url));
614
615
        // If using a real url, remove protocol / hostname / auth / port
616
        if (preg_match('#^(?<protocol>https?:)?//(?<hostpart>[^/]*)(?<url>(/.*)?)$#i', $url, $matches)) {
617
            $url = $matches['url'];
618
        }
619
620
        // Empty case
621
        if (trim($url, '\\/') === '') {
622
            return '';
623
        }
624
625
        // Remove base folder or url
626
        foreach ([self::baseFolder(), self::baseURL()] as $base) {
627
            // Ensure single / doesn't break comparison (unless it would make base empty)
628
            $base = rtrim($base, '\\/') ?: $base;
629
            if (stripos($url, $base) === 0) {
630
                return ltrim(substr($url, strlen($base)), '\\/');
631
            }
632
        }
633
634
        // Nothing matched, fall back to returning the original URL
635
        return $url;
636
    }
637
638
    /**
639
     * Returns true if a given path is absolute. Works under both *nix and windows systems.
640
     *
641
     * @param string $path
642
     *
643
     * @return bool
644
     */
645
    public static function is_absolute($path)
646
    {
647
        if (empty($path)) {
648
            return false;
649
        }
650
        if ($path[0] == '/' || $path[0] == '\\') {
651
            return true;
652
        }
653
        return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
654
    }
655
656
    /**
657
     * Determine if the url is root relative (i.e. starts with /, but not with //) SilverStripe
658
     * considers root relative urls as a subset of relative urls.
659
     *
660
     * @param string $url
661
     *
662
     * @return bool
663
     */
664
    public static function is_root_relative_url($url)
665
    {
666
        return strpos($url, '/') === 0 && strpos($url, '//') !== 0;
667
    }
668
669
    /**
670
     * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). URLs beginning with "//"
671
     * are treated as absolute, as browsers take this to mean the same protocol as currently being used.
672
     *
673
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
674
     * and avoid phishing attacks by redirecting to an attackers server.
675
     *
676
     * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
677
     * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
678
     *
679
     * @param string $url
680
     *
681
     * @return bool
682
     */
683
    public static function is_absolute_url($url)
684
    {
685
        // Strip off the query and fragment parts of the URL before checking
686
        if (($queryPosition = strpos($url, '?')) !== false) {
687
            $url = substr($url, 0, $queryPosition - 1);
688
        }
689
        if (($hashPosition = strpos($url, '#')) !== false) {
690
            $url = substr($url, 0, $hashPosition - 1);
691
        }
692
        $colonPosition = strpos($url, ':');
693
        $slashPosition = strpos($url, '/');
694
        return (
695
            // Base check for existence of a host on a compliant URL
696
            parse_url($url, PHP_URL_HOST)
697
            // Check for more than one leading slash without a protocol.
698
            // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
699
            // and hence a potential security risk. Single leading slashes are not an issue though.
700
            || preg_match('%^\s*/{2,}%', $url)
701
            || (
702
                // If a colon is found, check if it's part of a valid scheme definition
703
                // (meaning its not preceded by a slash).
704
                $colonPosition !== false
705
                && ($slashPosition === false || $colonPosition < $slashPosition)
706
            )
707
        );
708
    }
709
710
    /**
711
     * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()}.
712
     *
713
     * @param string $url
714
     *
715
     * @return bool
716
     */
717
    public static function is_relative_url($url)
718
    {
719
        return !static::is_absolute_url($url);
720
    }
721
722
    /**
723
     * Checks if the given URL is belonging to this "site" (not an external link). That's the case if
724
     * the URL is relative, as defined by {@link is_relative_url()}, or if the host matches
725
     * {@link protocolAndHost()}.
726
     *
727
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
728
     * and avoid phishing attacks by redirecting to an attackers server.
729
     *
730
     * @param string $url
731
     *
732
     * @return bool
733
     */
734
    public static function is_site_url($url)
735
    {
736
        $parsedURL = parse_url($url);
737
738
        // Validate user (disallow slashes)
739
        if (!empty($parsedURL['user']) && strstr($parsedURL['user'], '\\')) {
740
            return false;
741
        }
742
        if (!empty($parsedURL['pass']) && strstr($parsedURL['pass'], '\\')) {
743
            return false;
744
        }
745
746
        // 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...
747
        $actualHost = parse_url(self::protocolAndHost(), PHP_URL_HOST);
748
        if (!empty($parsedURL['host'])
749
            && $actualHost
750
            && $parsedURL['host'] === $actualHost
751
        ) {
752
            return true;
753
        }
754
755
        return self::is_relative_url($url);
756
    }
757
758
    /**
759
     * Given a filesystem reference relative to the site root, return the full file-system path.
760
     *
761
     * @param string $file
762
     *
763
     * @return string
764
     */
765
    public static function getAbsFile($file)
766
    {
767
        return self::is_absolute($file) ? $file : Director::baseFolder() . '/' . $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...
768
    }
769
770
    /**
771
     * Returns true if the given file exists. Filename should be relative to the site root.
772
     *
773
     * @param $file
774
     *
775
     * @return bool
776
     */
777
    public static function fileExists($file)
778
    {
779
        // replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
780
        $file = preg_replace('/([^\?]*)?.*/', '$1', $file);
781
        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...
782
    }
783
784
    /**
785
     * Returns the Absolute URL of the site root.
786
     *
787
     * @return string
788
     */
789
    public static function absoluteBaseURL()
790
    {
791
        return self::absoluteURL(
792
            self::baseURL(),
793
            self::ROOT
794
        );
795
    }
796
797
    /**
798
     * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
799
     * the URL.
800
     *
801
     * @param HTTPRequest|null $request
802
     * @return string
803
     */
804
    public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
805
    {
806
        $login = "";
807
808
        if (isset($_SERVER['PHP_AUTH_USER'])) {
809
            $login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@";
810
        }
811
812
        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...
813
    }
814
815
    /**
816
     * Skip any further processing and immediately respond with a redirect to the passed URL.
817
     *
818
     * @param string $destURL
819
     * @throws HTTPResponse_Exception
820
     */
821
    protected static function force_redirect($destURL)
822
    {
823
        // Redirect to installer
824
        $response = new HTTPResponse();
825
        $response->redirect($destURL, 301);
826
        HTTP::add_cache_headers($response);
827
        throw new HTTPResponse_Exception($response);
828
    }
829
830
    /**
831
     * Force the site to run on SSL.
832
     *
833
     * To use, call from _config.php. For example:
834
     * <code>
835
     * if (Director::isLive()) Director::forceSSL();
836
     * </code>
837
     *
838
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
839
     * patterns for matching relative URLs. For example:
840
     * <code>
841
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
842
     * </code>
843
     *
844
     * If you want certain parts of your site protected under a different domain, you can specify
845
     * the domain as an argument:
846
     * <code>
847
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
848
     * </code>
849
     *
850
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
851
     * to ensure that this won't cause usability problems.
852
     *
853
     * CAUTION: This does not respect the site environment mode. You should check this
854
     * as per the above examples using Director::isLive() or Director::isTest() for example.
855
     *
856
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
857
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
858
     * @param HTTPRequest|null $request Request object to check
859
     */
860
    public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
861
    {
862
        $handler = CanonicalURLMiddleware::singleton()->setForceSSL(true);
863
        if ($patterns) {
864
            $handler->setForceSSLPatterns($patterns);
865
        }
866
        if ($secureDomain) {
867
            $handler->setForceSSLDomain($secureDomain);
868
        }
869
        $handler->throwRedirectIfNeeded($request);
870
    }
871
872
    /**
873
     * Force a redirect to a domain starting with "www."
874
     *
875
     * @param HTTPRequest $request
876
     */
877
    public static function forceWWW(HTTPRequest $request = null)
878
    {
879
        $handler = CanonicalURLMiddleware::singleton()->setForceWWW(true);
880
        $handler->throwRedirectIfNeeded($request);
881
    }
882
883
    /**
884
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
885
     * jQuery or whether a manually set request-parameter 'ajax' is present.
886
     *
887
     * @param HTTPRequest $request
888
     * @return bool
889
     */
890
    public static function is_ajax(HTTPRequest $request = null)
891
    {
892
        $request = self::currentRequest($request);
893
        if ($request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
894
            return $request->isAjax();
895
        } else {
896
            return (
897
                isset($_REQUEST['ajax']) ||
898
                (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
899
            );
900
        }
901
    }
902
903
    /**
904
     * Returns true if this script is being run from the command line rather than the web server.
905
     *
906
     * @return bool
907
     */
908
    public static function is_cli()
909
    {
910
        return in_array(php_sapi_name(), ['cli', 'phpdbg']);
911
    }
912
913
    /**
914
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
915
     * {@link Director::isLive()}.
916
     *
917
     * @return string
918
     */
919
    public static function get_environment_type()
920
    {
921
        /** @var Kernel $kernel */
922
        $kernel = Injector::inst()->get(Kernel::class);
923
        return $kernel->getEnvironment();
924
    }
925
926
    /**
927
     * This function will return true if the site is in a live environment. For information about
928
     * environment types, see {@link Director::set_environment_type()}.
929
     *
930
     * @return bool
931
     */
932
    public static function isLive()
933
    {
934
        return self::get_environment_type() === 'live';
935
    }
936
937
    /**
938
     * This function will return true if the site is in a development environment. For information about
939
     * environment types, see {@link Director::set_environment_type()}.
940
     *
941
     * @return bool
942
     */
943
    public static function isDev()
944
    {
945
        return self::get_environment_type() === 'dev';
946
    }
947
948
    /**
949
     * This function will return true if the site is in a test environment. For information about
950
     * environment types, see {@link Director::set_environment_type()}.
951
     *
952
     * @return bool
953
     */
954
    public static function isTest()
955
    {
956
        return self::get_environment_type() === 'test';
957
    }
958
959
    /**
960
     * Returns an array of strings of the method names of methods on the call that should be exposed
961
     * as global variables in the templates.
962
     *
963
     * @return array
964
     */
965
    public static function get_template_global_variables()
966
    {
967
        return array(
968
            'absoluteBaseURL',
969
            'baseURL',
970
            'is_ajax',
971
            'isAjax' => 'is_ajax',
972
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
973
        );
974
    }
975
976
    /**
977
     * Helper to validate or check the current request object
978
     *
979
     * @param HTTPRequest $request
980
     * @return HTTPRequest Request object if one is both current and valid
981
     */
982
    protected static function currentRequest(HTTPRequest $request = null)
983
    {
984
        // Ensure we only use a registered HTTPRequest and don't
985
        // incidentally construct a singleton
986
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
987
            $request = Injector::inst()->get(HTTPRequest::class);
988
        }
989
        return $request;
990
    }
991
}
992