Director::mockRequest()   D
last analyzed

Complexity

Conditions 17
Paths 128

Size

Total Lines 120
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 58
nc 128
nop 9
dl 0
loc 120
rs 4.9833
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTree was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
6
use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
7
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Environment;
10
use SilverStripe\Core\Extensible;
11
use SilverStripe\Core\Injector\Injectable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Kernel;
14
use SilverStripe\Core\Path;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\Versioned\Versioned;
17
use SilverStripe\View\Requirements;
18
use SilverStripe\View\Requirements_Backend;
19
use SilverStripe\View\TemplateGlobalProvider;
20
21
/**
22
 * Director is responsible for processing URLs, and providing environment information.
23
 *
24
 * The most important part of director is {@link Director::handleRequest()}, which is passed an HTTPRequest and will
25
 * execute the appropriate controller.
26
 *
27
 * @see Director::handleRequest()
28
 * @see Director::$rules
29
 * @skipUpgrade
30
 */
31
class Director implements TemplateGlobalProvider
32
{
33
    use Configurable;
34
    use Extensible;
35
    use Injectable;
36
    use HTTPMiddlewareAware;
37
38
    /**
39
     * Specifies this url is relative to the base.
40
     *
41
     * @var string
42
     */
43
    const BASE = 'BASE';
44
45
    /**
46
     * Specifies this url is relative to the site root.
47
     *
48
     * @var string
49
     */
50
    const ROOT = 'ROOT';
51
52
    /**
53
     * specifies this url is relative to the current request.
54
     *
55
     * @var string
56
     */
57
    const REQUEST = 'REQUEST';
58
59
    /**
60
     * @config
61
     * @var array
62
     */
63
    private static $rules = array();
0 ignored issues
show
introduced by
The private property $rules is not used, and could be removed.
Loading history...
64
65
    /**
66
     * Set current page
67
     *
68
     * @internal
69
     * @var SiteTree
70
     */
71
    private static $current_page;
72
73
    /**
74
     * @config
75
     * @var string
76
     */
77
    private static $alternate_base_folder;
0 ignored issues
show
introduced by
The private property $alternate_base_folder is not used, and could be removed.
Loading history...
78
79
    /**
80
     * Override PUBLIC_DIR. Set to a non-null value to override.
81
     * Setting to an empty string will disable public dir.
82
     *
83
     * @config
84
     * @var bool|null
85
     */
86
    private static $alternate_public_dir = null;
0 ignored issues
show
introduced by
The private property $alternate_public_dir is not used, and could be removed.
Loading history...
87
88
    /**
89
     * Base url to populate if cannot be determined otherwise.
90
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
91
     *
92
     * @config
93
     * @var string
94
     */
95
    private static $default_base_url = '`SS_BASE_URL`';
0 ignored issues
show
introduced by
The private property $default_base_url is not used, and could be removed.
Loading history...
96
97
    public function __construct()
98
    {
99
    }
100
101
    /**
102
     * Test a URL request, returning a response object. This method is a wrapper around
103
     * Director::handleRequest() to assist with functional testing. It will execute the URL given, and
104
     * return the result as an HTTPResponse object.
105
     *
106
     * @param string $url The URL to visit.
107
     * @param array $postVars The $_POST & $_FILES variables.
108
     * @param array|Session $session The {@link Session} object representing the current session.
109
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
110
     * session.
111
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
112
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
113
     * @param string $body The HTTP body.
114
     * @param array $headers HTTP headers with key-value pairs.
115
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
116
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
117
     *
118
     * @return HTTPResponse
119
     *
120
     * @throws HTTPResponse_Exception
121
     */
122
    public static function test(
123
        $url,
124
        $postVars = [],
125
        $session = array(),
126
        $httpMethod = null,
127
        $body = null,
128
        $headers = array(),
129
        $cookies = array(),
130
        &$request = null
131
    ) {
132
        return static::mockRequest(
133
            function (HTTPRequest $request) {
134
                return Director::singleton()->handleRequest($request);
135
            },
136
            $url,
137
            $postVars,
138
            $session,
139
            $httpMethod,
140
            $body,
141
            $headers,
142
            $cookies,
143
            $request
144
        );
145
    }
146
147
    /**
148
     * Mock a request, passing this to the given callback, before resetting.
149
     *
150
     * @param callable $callback Action to pass the HTTPRequst object
151
     * @param string $url The URL to build
152
     * @param array $postVars The $_POST & $_FILES variables.
153
     * @param array|Session $session The {@link Session} object representing the current session.
154
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
155
     * session.
156
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
157
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
158
     * @param string $body The HTTP body.
159
     * @param array $headers HTTP headers with key-value pairs.
160
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
161
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
162
     * @return mixed Result of callback
163
     */
164
    public static function mockRequest(
165
        $callback,
166
        $url,
167
        $postVars = [],
168
        $session = [],
169
        $httpMethod = null,
170
        $body = null,
171
        $headers = [],
172
        $cookies = [],
173
        &$request = null
174
    ) {
175
        // Build list of cleanup promises
176
        $finally = [];
177
178
        /** @var Kernel $kernel */
179
        $kernel = Injector::inst()->get(Kernel::class);
180
        $kernel->nest();
181
        $finally[] = function () use ($kernel) {
182
            $kernel->activate();
183
        };
184
185
        // backup existing vars, and create new vars
186
        $existingVars = Environment::getVariables();
187
        $finally[] = function () use ($existingVars) {
188
            Environment::setVariables($existingVars);
189
        };
190
        $newVars = $existingVars;
191
192
        // These are needed so that calling Director::test() does not muck with whoever is calling it.
193
        // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
194
        if (class_exists(Versioned::class)) {
195
            $oldReadingMode = Versioned::get_reading_mode();
196
            $finally[] = function () use ($oldReadingMode) {
197
                Versioned::set_reading_mode($oldReadingMode);
198
            };
199
        }
200
201
        // Default httpMethod
202
        $newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod ?: ($postVars ? "POST" : "GET");
203
        $newVars['_POST'] = (array)$postVars;
204
205
        // Setup session
206
        if ($session instanceof Session) {
207
            // Note: If passing $session as object, ensure that changes are written back
208
            // This is important for classes such as FunctionalTest which emulate cross-request persistence
209
            $newVars['_SESSION'] = $sessionArray = $session->getAll() ?: [];
210
            $finally[] = function () use ($session, $sessionArray) {
211
                if (isset($_SESSION)) {
212
                    // Set new / updated keys
213
                    foreach ($_SESSION as $key => $value) {
214
                        $session->set($key, $value);
215
                    }
216
                    // Unset removed keys
217
                    foreach (array_diff_key($sessionArray, $_SESSION) as $key => $value) {
218
                        $session->clear($key);
219
                    }
220
                }
221
            };
222
        } else {
223
            $newVars['_SESSION'] = $session ?: [];
224
        }
225
226
        // Setup cookies
227
        $cookieJar = $cookies instanceof Cookie_Backend
228
            ? $cookies
229
            : Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: []));
230
        $newVars['_COOKIE'] = $cookieJar->getAll(false);
231
        Cookie::config()->update('report_errors', false);
232
        Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
233
234
        // Backup requirements
235
        $existingRequirementsBackend = Requirements::backend();
236
        Requirements::set_backend(Requirements_Backend::create());
237
        $finally[] = function () use ($existingRequirementsBackend) {
238
            Requirements::set_backend($existingRequirementsBackend);
239
        };
240
241
        // Strip any hash
242
        $url = strtok($url, '#');
243
244
        // Handle absolute URLs
245
        // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
246
        $urlHostPort = static::parseHost($url);
247
        if ($urlHostPort) {
248
            $newVars['_SERVER']['HTTP_HOST'] = $urlHostPort;
249
        }
250
251
        // Ensure URL is properly made relative.
252
        // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
253
        $url = self::makeRelative($url);
254
        if (strpos($url, '?') !== false) {
255
            list($url, $getVarsEncoded) = explode('?', $url, 2);
256
            parse_str($getVarsEncoded, $newVars['_GET']);
257
        } else {
258
            $newVars['_GET'] = [];
259
        }
260
        $newVars['_SERVER']['REQUEST_URI'] = Director::baseURL() . ltrim($url, '/');
261
        $newVars['_REQUEST'] = array_merge($newVars['_GET'], $newVars['_POST']);
262
263
        // Normalise vars
264
        $newVars = HTTPRequestBuilder::cleanEnvironment($newVars);
265
266
        // Create new request
267
        $request = HTTPRequestBuilder::createFromVariables($newVars, $body, ltrim($url, '/'));
268
        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...
269
            foreach ($headers as $k => $v) {
270
                $request->addHeader($k, $v);
271
            }
272
        }
273
274
        // Apply new vars to environment
275
        Environment::setVariables($newVars);
276
277
        try {
278
            // Normal request handling
279
            return call_user_func($callback, $request);
280
        } finally {
281
            // Restore state in reverse order to assignment
282
            foreach (array_reverse($finally) as $callback) {
0 ignored issues
show
introduced by
$callback is overwriting one of the parameters of this function.
Loading history...
283
                call_user_func($callback);
284
            }
285
        }
286
    }
287
288
    /**
289
     * Process the given URL, creating the appropriate controller and executing it.
290
     *
291
     * Request processing is handled as follows:
292
     * - Director::handleRequest($request) checks each of the Director rules and identifies a controller
293
     *   to handle this request.
294
     * - Controller::handleRequest($request) is then called.  This will find a rule to handle the URL,
295
     *   and call the rule handling method.
296
     * - RequestHandler::handleRequest($request) is recursively called whenever a rule handling method
297
     *   returns a RequestHandler object.
298
     *
299
     * In addition to request processing, Director will manage the session, and perform the output of
300
     * the actual response to the browser.
301
     *
302
     * @param HTTPRequest $request
303
     * @return HTTPResponse
304
     * @throws HTTPResponse_Exception
305
     */
306
    public function handleRequest(HTTPRequest $request)
307
    {
308
        Injector::inst()->registerService($request, HTTPRequest::class);
309
310
        $rules = Director::config()->uninherited('rules');
311
312
        $this->extend('updateRules', $rules);
313
314
        // Default handler - mo URL rules matched, so return a 404 error.
315
        $handler = function () {
316
            return new HTTPResponse('No URL rule was matched', 404);
317
        };
318
319
        foreach ($rules as $pattern => $controllerOptions) {
320
            // Match pattern
321
            $arguments = $request->match($pattern, true);
322
            if ($arguments == false) {
323
                continue;
324
            }
325
326
            // Normalise route rule
327
            if (is_string($controllerOptions)) {
328
                if (substr($controllerOptions, 0, 2) == '->') {
329
                    $controllerOptions = array('Redirect' => substr($controllerOptions, 2));
330
                } else {
331
                    $controllerOptions = array('Controller' => $controllerOptions);
332
                }
333
            }
334
            $request->setRouteParams($controllerOptions);
335
336
            // controllerOptions provide some default arguments
337
            $arguments = array_merge($controllerOptions, $arguments);
338
339
            // Pop additional tokens from the tokenizer if necessary
340
            if (isset($controllerOptions['_PopTokeniser'])) {
341
                $request->shift($controllerOptions['_PopTokeniser']);
342
            }
343
344
            // Handler for redirection
345
            if (isset($arguments['Redirect'])) {
346
                $handler = function () use ($arguments) {
347
                    // Redirection
348
                    $response = new HTTPResponse();
349
                    $response->redirect(static::absoluteURL($arguments['Redirect']));
350
                    return $response;
351
                };
352
                break;
353
            }
354
355
            // Handler for constructing and calling a controller
356
            $handler = function (HTTPRequest $request) use ($arguments) {
357
                try {
358
                    /** @var RequestHandler $controllerObj */
359
                    $controllerObj = Injector::inst()->create($arguments['Controller']);
360
                    return $controllerObj->handleRequest($request);
361
                } catch (HTTPResponse_Exception $responseException) {
362
                    return $responseException->getResponse();
363
                }
364
            };
365
            break;
366
        }
367
368
        // Call the handler with the configured middlewares
369
        $response = $this->callMiddleware($request, $handler);
370
371
        // Note that if a different request was previously registered, this will now be lost
372
        // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
373
        Injector::inst()->unregisterNamedObject(HTTPRequest::class);
374
375
        return $response;
376
    }
377
378
    /**
379
     * Returns indication whether the manifest cache has been flushed
380
     * in the beginning of the current request.
381
     *
382
     * That could mean the current active request has `?flush` parameter.
383
     * Another possibility is a race condition when the current request
384
     * hits the server in between another request `?flush` authorisation
385
     * and a redirect to the actual flush.
386
     *
387
     * @return bool
388
     *
389
     * @deprecated 5.0 Kernel::isFlushed to be used instead
390
     */
391
    public static function isManifestFlushed()
392
    {
393
        $kernel = Injector::inst()->get(Kernel::class);
394
395
        // Only CoreKernel implements this method at the moment
396
        // Introducing it to the Kernel interface is a breaking change
397
        if (method_exists($kernel, 'isFlushed')) {
398
            return $kernel->isFlushed();
399
        }
400
401
        $classManifest = $kernel->getClassLoader()->getManifest();
402
        return $classManifest->isFlushed();
403
    }
404
405
    /**
406
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
407
     * object to return, then this will return the current controller.
408
     *
409
     * @return SiteTree|Controller
410
     */
411
    public static function get_current_page()
412
    {
413
        return self::$current_page ? self::$current_page : Controller::curr();
414
    }
415
416
    /**
417
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
418
     *
419
     * @param SiteTree $page
420
     */
421
    public static function set_current_page($page)
422
    {
423
        self::$current_page = $page;
424
    }
425
426
    /**
427
     * Converts the given path or url into an absolute url. This method follows the below rules:
428
     * - Absolute urls (e.g. `http://localhost`) are not modified
429
     * - Relative urls (e.g. `//localhost`) have current protocol added (`http://localhost`)
430
     * - Absolute paths (e.g. `/base/about-us`) are resolved by adding the current protocol and
431
     *   host (`http://localhost/base/about-us`)
432
     * - Relative paths (e.g. `about-us/staff`) must be resolved using one of three methods, disambiguated via
433
     *   the $relativeParent argument:
434
     *     - BASE - Append this path to the base url (i.e. behaves as though `<base>` tag is provided in a html
435
     *       document). This is the default.
436
     *     - REQUEST - Resolve this path to the current url (i.e. behaves as though no `<base>` tag is provided in
437
     *       a html document)
438
     *     - ROOT - Treat this as though it was an absolute path, and append it to the protocol and hostname.
439
     *
440
     * @param string $url The url or path to resolve to absolute url.
441
     * @param string $relativeParent Disambiguation method to use for evaluating relative paths
442
     * @return string The absolute url
443
     */
444
    public static function absoluteURL($url, $relativeParent = self::BASE)
445
    {
446
        if (is_bool($relativeParent)) {
0 ignored issues
show
introduced by
The condition is_bool($relativeParent) is always false.
Loading history...
447
            // Deprecate old boolean second parameter
448
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
449
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
450
        }
451
452
        // Check if there is already a protocol given
453
        if (preg_match('/^http(s?):\/\//', $url)) {
454
            return $url;
455
        }
456
457
        // Absolute urls without protocol are added
458
        // E.g. //google.com -> http://google.com
459
        if (strpos($url, '//') === 0) {
460
            return self::protocol() . substr($url, 2);
461
        }
462
463
        // Determine method for mapping the parent to this relative url
464
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
465
            // Root relative urls always should be evaluated relative to the root
466
            $parent = self::protocolAndHost();
467
        } elseif ($relativeParent === self::REQUEST) {
468
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
469
            if (!isset($_SERVER['REQUEST_URI'])) {
470
                return false;
471
            }
472
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
473
        } else {
474
            // Default to respecting site base_url
475
            $parent = self::absoluteBaseURL();
476
        }
477
478
        // Map empty urls to relative slash and join to base
479
        if (empty($url) || $url === '.' || $url === './') {
480
            $url = '/';
481
        }
482
        return Controller::join_links($parent, $url);
483
    }
484
485
    /**
486
     * Return only host (and optional port) part of a url
487
     *
488
     * @param string $url
489
     * @return string|null Hostname, and optional port, or null if not a valid host
490
     */
491
    protected static function parseHost($url)
492
    {
493
        // Get base hostname
494
        $host = parse_url($url, PHP_URL_HOST);
495
        if (!$host) {
496
            return null;
497
        }
498
499
        // Include port
500
        $port = parse_url($url, PHP_URL_PORT);
501
        if ($port) {
502
            $host .= ':' . $port;
503
        }
504
505
        return $host;
506
    }
507
508
    /**
509
     * Validate user and password in URL, disallowing slashes
510
     *
511
     * @param string $url
512
     * @return bool
513
     */
514
    protected static function validateUserAndPass($url)
515
    {
516
        $parsedURL = parse_url($url);
517
518
        // Validate user (disallow slashes)
519
        if (!empty($parsedURL['user']) && strstr($parsedURL['user'], '\\')) {
520
            return false;
521
        }
522
        if (!empty($parsedURL['pass']) && strstr($parsedURL['pass'], '\\')) {
523
            return false;
524
        }
525
526
        return true;
527
    }
528
529
    /**
530
     * A helper to determine the current hostname used to access the site.
531
     * The following are used to determine the host (in order)
532
     *  - Director.alternate_base_url (if it contains a domain name)
533
     *  - Trusted proxy headers
534
     *  - HTTP Host header
535
     *  - SS_BASE_URL env var
536
     *  - SERVER_NAME
537
     *  - gethostname()
538
     *
539
     * @param HTTPRequest $request
540
     * @return string Host name, including port (if present)
541
     */
542
    public static function host(HTTPRequest $request = null)
543
    {
544
        // Check if overridden by alternate_base_url
545
        if ($baseURL = self::config()->get('alternate_base_url')) {
546
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
547
            $host = static::parseHost($baseURL);
0 ignored issues
show
Bug introduced by
It seems like $baseURL can also be of type array; however, parameter $url of SilverStripe\Control\Director::parseHost() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

547
            $host = static::parseHost(/** @scrutinizer ignore-type */ $baseURL);
Loading history...
548
            if ($host) {
549
                return $host;
550
            }
551
        }
552
553
        $request = static::currentRequest($request);
554
        if ($request && ($host = $request->getHeader('Host'))) {
555
            return $host;
556
        }
557
558
        // Check given header
559
        if (isset($_SERVER['HTTP_HOST'])) {
560
            return $_SERVER['HTTP_HOST'];
561
        }
562
563
        // Check base url
564
        if ($baseURL = self::config()->uninherited('default_base_url')) {
565
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
566
            $host = static::parseHost($baseURL);
567
            if ($host) {
568
                return $host;
569
            }
570
        }
571
572
        // Fail over to server_name (least reliable)
573
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
574
    }
575
576
    /**
577
     * Return port used for the base URL.
578
     * Note, this will be null if not specified, in which case you should assume the default
579
     * port for the current protocol.
580
     *
581
     * @param HTTPRequest $request
582
     * @return int|null
583
     */
584
    public static function port(HTTPRequest $request = null)
585
    {
586
        $host = static::host($request);
587
        return (int)parse_url($host, PHP_URL_PORT) ?: null;
588
    }
589
590
    /**
591
     * Return host name without port
592
     *
593
     * @param HTTPRequest|null $request
594
     * @return string|null
595
     */
596
    public static function hostName(HTTPRequest $request = null)
597
    {
598
        $host = static::host($request);
599
        return parse_url($host, PHP_URL_HOST) ?: null;
600
    }
601
602
    /**
603
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
604
     * variable isn't set.
605
     *
606
     * @param HTTPRequest $request
607
     * @return bool|string
608
     */
609
    public static function protocolAndHost(HTTPRequest $request = null)
610
    {
611
        return static::protocol($request) . static::host($request);
612
    }
613
614
    /**
615
     * Return the current protocol that the site is running under.
616
     *
617
     * @param HTTPRequest $request
618
     * @return string
619
     */
620
    public static function protocol(HTTPRequest $request = null)
621
    {
622
        return (self::is_https($request)) ? 'https://' : 'http://';
623
    }
624
625
    /**
626
     * Return whether the site is running as under HTTPS.
627
     *
628
     * @param HTTPRequest $request
629
     * @return bool
630
     */
631
    public static function is_https(HTTPRequest $request = null)
632
    {
633
        // Check override from alternate_base_url
634
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
635
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
636
            $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

636
            $protocol = parse_url(/** @scrutinizer ignore-type */ $baseURL, PHP_URL_SCHEME);
Loading history...
637
            if ($protocol) {
638
                return $protocol === 'https';
639
            }
640
        }
641
642
        // Check the current request
643
        $request = static::currentRequest($request);
644
        if ($request && ($scheme = $request->getScheme())) {
645
            return $scheme === 'https';
646
        }
647
648
        // Check default_base_url
649
        if ($baseURL = self::config()->uninherited('default_base_url')) {
650
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
651
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
652
            if ($protocol) {
653
                return $protocol === 'https';
654
            }
655
        }
656
657
        return false;
658
    }
659
660
    /**
661
     * Return the root-relative url for the baseurl
662
     *
663
     * @return string Root-relative url with trailing slash.
664
     */
665
    public static function baseURL()
666
    {
667
        // Check override base_url
668
        $alternate = self::config()->get('alternate_base_url');
669
        if ($alternate) {
670
            $alternate = Injector::inst()->convertServiceProperty($alternate);
671
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
672
        }
673
674
        // Get env base url
675
        $baseURL = rtrim(BASE_URL, '/') . '/';
676
677
        // Check if BASE_SCRIPT_URL is defined
678
        // e.g. `index.php/`
679
        if (defined('BASE_SCRIPT_URL')) {
680
            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...
681
        }
682
683
        return $baseURL;
684
    }
685
686
    /**
687
     * Returns the root filesystem folder for the site. It will be automatically calculated unless
688
     * it is overridden with {@link setBaseFolder()}.
689
     *
690
     * @return string
691
     */
692
    public static function baseFolder()
693
    {
694
        $alternate = Director::config()->uninherited('alternate_base_folder');
695
        return $alternate ?: BASE_PATH;
696
    }
697
698
    /**
699
     * Check if using a seperate public dir, and if so return this directory
700
     * name.
701
     *
702
     * This will be removed in 5.0 and fixed to 'public'
703
     *
704
     * @return string
705
     */
706
    public static function publicDir()
707
    {
708
        $alternate = self::config()->uninherited('alternate_public_dir');
709
        if (isset($alternate)) {
710
            return $alternate;
711
        }
712
        return PUBLIC_DIR;
713
    }
714
715
    /**
716
     * Gets the webroot of the project, which may be a subfolder of {@see baseFolder()}
717
     *
718
     * @return string
719
     */
720
    public static function publicFolder()
721
    {
722
        $folder = self::baseFolder();
723
        $publicDir = self::publicDir();
724
        if ($publicDir) {
725
            return Path::join($folder, $publicDir);
726
        }
727
728
        return $folder;
729
    }
730
731
    /**
732
     * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
733
     * when turning a URL into a filesystem reference, or vice versa.
734
     *
735
     * Note: You should check {@link Director::is_site_url()} if making an untrusted url relative prior
736
     * to calling this function.
737
     *
738
     * @param string $url Accepts both a URL or a filesystem path.
739
     * @return string
740
     */
741
    public static function makeRelative($url)
742
    {
743
        // Allow for the accidental inclusion whitespace and // in the URL
744
        $url = preg_replace('#([^:])//#', '\\1/', trim($url));
745
746
        // If using a real url, remove protocol / hostname / auth / port
747
        if (preg_match('#^(?<protocol>https?:)?//(?<hostpart>[^/]*)(?<url>(/.*)?)$#i', $url, $matches)) {
748
            $url = $matches['url'];
749
        }
750
751
        // Empty case
752
        if (trim($url, '\\/') === '') {
753
            return '';
754
        }
755
756
        // Remove base folder or url
757
        foreach ([self::publicFolder(), self::baseFolder(), self::baseURL()] as $base) {
758
            // Ensure single / doesn't break comparison (unless it would make base empty)
759
            $base = rtrim($base, '\\/') ?: $base;
760
            if (stripos($url, $base) === 0) {
761
                return ltrim(substr($url, strlen($base)), '\\/');
762
            }
763
        }
764
765
        // Nothing matched, fall back to returning the original URL
766
        return $url;
767
    }
768
769
    /**
770
     * Returns true if a given path is absolute. Works under both *nix and windows systems.
771
     *
772
     * @param string $path
773
     *
774
     * @return bool
775
     */
776
    public static function is_absolute($path)
777
    {
778
        if (empty($path)) {
779
            return false;
780
        }
781
        if ($path[0] == '/' || $path[0] == '\\') {
782
            return true;
783
        }
784
        return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
785
    }
786
787
    /**
788
     * Determine if the url is root relative (i.e. starts with /, but not with //) SilverStripe
789
     * considers root relative urls as a subset of relative urls.
790
     *
791
     * @param string $url
792
     *
793
     * @return bool
794
     */
795
    public static function is_root_relative_url($url)
796
    {
797
        return strpos($url, '/') === 0 && strpos($url, '//') !== 0;
798
    }
799
800
    /**
801
     * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). URLs beginning with "//"
802
     * are treated as absolute, as browsers take this to mean the same protocol as currently being used.
803
     *
804
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
805
     * and avoid phishing attacks by redirecting to an attackers server.
806
     *
807
     * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
808
     * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
809
     *
810
     * @param string $url
811
     *
812
     * @return bool
813
     */
814
    public static function is_absolute_url($url)
815
    {
816
        // Strip off the query and fragment parts of the URL before checking
817
        if (($queryPosition = strpos($url, '?')) !== false) {
818
            $url = substr($url, 0, $queryPosition - 1);
819
        }
820
        if (($hashPosition = strpos($url, '#')) !== false) {
821
            $url = substr($url, 0, $hashPosition - 1);
822
        }
823
        $colonPosition = strpos($url, ':');
824
        $slashPosition = strpos($url, '/');
825
        return (
826
            // Base check for existence of a host on a compliant URL
827
            parse_url($url, PHP_URL_HOST)
828
            // Check for more than one leading slash without a protocol.
829
            // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
830
            // and hence a potential security risk. Single leading slashes are not an issue though.
831
            || preg_match('%^\s*/{2,}%', $url)
832
            || (
833
                // If a colon is found, check if it's part of a valid scheme definition
834
                // (meaning its not preceded by a slash).
835
                $colonPosition !== false
836
                && ($slashPosition === false || $colonPosition < $slashPosition)
837
            )
838
        );
839
    }
840
841
    /**
842
     * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()}.
843
     *
844
     * @param string $url
845
     *
846
     * @return bool
847
     */
848
    public static function is_relative_url($url)
849
    {
850
        return !static::is_absolute_url($url);
851
    }
852
853
    /**
854
     * Checks if the given URL is belonging to this "site" (not an external link). That's the case if
855
     * the URL is relative, as defined by {@link is_relative_url()}, or if the host matches
856
     * {@link protocolAndHost()}.
857
     *
858
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
859
     * and avoid phishing attacks by redirecting to an attackers server.
860
     *
861
     * @param string $url
862
     *
863
     * @return bool
864
     */
865
    public static function is_site_url($url)
866
    {
867
        // Validate user and password
868
        if (!static::validateUserAndPass($url)) {
869
            return false;
870
        }
871
872
        // Validate host[:port]
873
        $urlHost = static::parseHost($url);
874
        if ($urlHost && $urlHost === static::host()) {
875
            return true;
876
        }
877
878
        // Relative urls always are site urls
879
        return self::is_relative_url($url);
880
    }
881
882
    /**
883
     * Given a filesystem reference relative to the site root, return the full file-system path.
884
     *
885
     * @param string $file
886
     *
887
     * @return string
888
     */
889
    public static function getAbsFile($file)
890
    {
891
        // If already absolute
892
        if (self::is_absolute($file)) {
893
            return $file;
894
        }
895
896
        // If path is relative to public folder search there first
897
        if (self::publicDir()) {
898
            $path = Path::join(self::publicFolder(), $file);
899
            if (file_exists($path)) {
900
                return $path;
901
            }
902
        }
903
904
        // Default to base folder
905
        return Path::join(self::baseFolder(), $file);
906
    }
907
908
    /**
909
     * Returns true if the given file exists. Filename should be relative to the site root.
910
     *
911
     * @param $file
912
     *
913
     * @return bool
914
     */
915
    public static function fileExists($file)
916
    {
917
        // replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
918
        $file = preg_replace('/([^\?]*)?.*/', '$1', $file);
919
        return file_exists(Director::getAbsFile($file));
920
    }
921
922
    /**
923
     * Returns the Absolute URL of the site root.
924
     *
925
     * @return string
926
     */
927
    public static function absoluteBaseURL()
928
    {
929
        return self::absoluteURL(
930
            self::baseURL(),
931
            self::ROOT
932
        );
933
    }
934
935
    /**
936
     * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
937
     * the URL.
938
     *
939
     * @param HTTPRequest|null $request
940
     * @return string
941
     */
942
    public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
943
    {
944
        // Detect basic auth
945
        $user = $request->getHeader('PHP_AUTH_USER');
0 ignored issues
show
Bug introduced by
The method getHeader() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

Loading history...
946
        if ($user) {
947
            $password = $request->getHeader('PHP_AUTH_PW');
948
            $login = sprintf("%s:%s@", $user, $password) ;
949
        } else {
950
            $login = '';
951
        }
952
953
        return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
954
    }
955
956
    /**
957
     * Skip any further processing and immediately respond with a redirect to the passed URL.
958
     *
959
     * @param string $destURL
960
     * @throws HTTPResponse_Exception
961
     */
962
    protected static function force_redirect($destURL)
963
    {
964
        // Redirect to installer
965
        $response = new HTTPResponse();
966
        $response->redirect($destURL, 301);
967
        throw new HTTPResponse_Exception($response);
968
    }
969
970
    /**
971
     * Force the site to run on SSL.
972
     *
973
     * To use, call from the init() method of your PageController. For example:
974
     * <code>
975
     * if (Director::isLive()) Director::forceSSL();
976
     * </code>
977
     *
978
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
979
     * patterns for matching relative URLs. For example:
980
     * <code>
981
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
982
     * </code>
983
     *
984
     * If you want certain parts of your site protected under a different domain, you can specify
985
     * the domain as an argument:
986
     * <code>
987
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
988
     * </code>
989
     *
990
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
991
     * to ensure that this won't cause usability problems.
992
     *
993
     * CAUTION: This does not respect the site environment mode. You should check this
994
     * as per the above examples using Director::isLive() or Director::isTest() for example.
995
     *
996
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
997
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
998
     * Can include port number.
999
     * @param HTTPRequest|null $request Request object to check
1000
     */
1001
    public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
1002
    {
1003
        $handler = CanonicalURLMiddleware::singleton()->setForceSSL(true);
1004
        if ($patterns) {
1005
            $handler->setForceSSLPatterns($patterns);
1006
        }
1007
        if ($secureDomain) {
1008
            $handler->setForceSSLDomain($secureDomain);
1009
        }
1010
        $handler->throwRedirectIfNeeded($request);
1011
    }
1012
1013
    /**
1014
     * Force a redirect to a domain starting with "www."
1015
     *
1016
     * @param HTTPRequest $request
1017
     */
1018
    public static function forceWWW(HTTPRequest $request = null)
1019
    {
1020
        $handler = CanonicalURLMiddleware::singleton()->setForceWWW(true);
1021
        $handler->throwRedirectIfNeeded($request);
1022
    }
1023
1024
    /**
1025
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
1026
     * jQuery or whether a manually set request-parameter 'ajax' is present.
1027
     *
1028
     * Note that if you plan to use this to alter your HTTP response on a cached page,
1029
     * you should add X-Requested-With to the Vary header.
1030
     *
1031
     * @param HTTPRequest $request
1032
     * @return bool
1033
     */
1034
    public static function is_ajax(HTTPRequest $request = null)
1035
    {
1036
        $request = self::currentRequest($request);
1037
        if ($request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
1038
            return $request->isAjax();
1039
        }
1040
1041
        return (
1042
            isset($_REQUEST['ajax']) ||
1043
            (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
1044
        );
1045
    }
1046
1047
    /**
1048
     * Returns true if this script is being run from the command line rather than the web server.
1049
     *
1050
     * @return bool
1051
     */
1052
    public static function is_cli()
1053
    {
1054
        return Environment::isCli();
1055
    }
1056
1057
    /**
1058
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
1059
     * {@link Director::isLive()}.
1060
     *
1061
     * @return string
1062
     */
1063
    public static function get_environment_type()
1064
    {
1065
        /** @var Kernel $kernel */
1066
        $kernel = Injector::inst()->get(Kernel::class);
1067
        return $kernel->getEnvironment();
1068
    }
1069
1070
1071
    /**
1072
     * Returns the session environment override
1073
     *
1074
     * @internal This method is not a part of public API and will be deleted without a deprecation warning
1075
     *
1076
     * @param HTTPRequest $request
1077
     *
1078
     * @return string|null null if not overridden, otherwise the actual value
1079
     */
1080
    public static function get_session_environment_type(HTTPRequest $request = null)
1081
    {
1082
        $request = static::currentRequest($request);
1083
1084
        if (!$request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
1085
            return null;
1086
        }
1087
1088
        $session = $request->getSession();
1089
1090
        if (!empty($session->get('isDev'))) {
1091
            return Kernel::DEV;
1092
        } elseif (!empty($session->get('isTest'))) {
1093
            return Kernel::TEST;
1094
        }
1095
    }
1096
1097
    /**
1098
     * This function will return true if the site is in a live environment. For information about
1099
     * environment types, see {@link Director::set_environment_type()}.
1100
     *
1101
     * @return bool
1102
     */
1103
    public static function isLive()
1104
    {
1105
        return self::get_environment_type() === 'live';
1106
    }
1107
1108
    /**
1109
     * This function will return true if the site is in a development environment. For information about
1110
     * environment types, see {@link Director::set_environment_type()}.
1111
     *
1112
     * @return bool
1113
     */
1114
    public static function isDev()
1115
    {
1116
        return self::get_environment_type() === 'dev';
1117
    }
1118
1119
    /**
1120
     * This function will return true if the site is in a test environment. For information about
1121
     * environment types, see {@link Director::set_environment_type()}.
1122
     *
1123
     * @return bool
1124
     */
1125
    public static function isTest()
1126
    {
1127
        return self::get_environment_type() === 'test';
1128
    }
1129
1130
    /**
1131
     * Returns an array of strings of the method names of methods on the call that should be exposed
1132
     * as global variables in the templates.
1133
     *
1134
     * @return array
1135
     */
1136
    public static function get_template_global_variables()
1137
    {
1138
        return array(
1139
            'absoluteBaseURL',
1140
            'baseURL',
1141
            'is_ajax',
1142
            'isAjax' => 'is_ajax',
1143
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1144
        );
1145
    }
1146
1147
    /**
1148
     * Helper to validate or check the current request object
1149
     *
1150
     * @param HTTPRequest $request
1151
     * @return HTTPRequest Request object if one is both current and valid
1152
     */
1153
    protected static function currentRequest(HTTPRequest $request = null)
1154
    {
1155
        // Ensure we only use a registered HTTPRequest and don't
1156
        // incidentally construct a singleton
1157
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
1158
            $request = Injector::inst()->get(HTTPRequest::class);
1159
        }
1160
        return $request;
1161
    }
1162
}
1163