Passed
Push — phpunit8 ( f24111 )
by Sam
08:30
created

Director::isManifestFlushed()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
6
use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
7
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Environment;
10
use SilverStripe\Core\Extensible;
11
use SilverStripe\Core\Injector\Injectable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Kernel;
14
use SilverStripe\Core\Path;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\Versioned\Versioned;
17
use SilverStripe\View\Requirements;
18
use SilverStripe\View\Requirements_Backend;
19
use SilverStripe\View\TemplateGlobalProvider;
20
21
/**
22
 * Director is responsible for processing URLs, and providing environment information.
23
 *
24
 * The most important part of director is {@link Director::handleRequest()}, which is passed an HTTPRequest and will
25
 * execute the appropriate controller.
26
 *
27
 * @see Director::handleRequest()
28
 * @see Director::$rules
29
 * @skipUpgrade
30
 */
31
class Director implements TemplateGlobalProvider
32
{
33
    use Configurable;
34
    use Extensible;
35
    use Injectable;
36
    use HTTPMiddlewareAware;
37
38
    /**
39
     * Specifies this url is relative to the base.
40
     *
41
     * @var string
42
     */
43
    const BASE = 'BASE';
44
45
    /**
46
     * Specifies this url is relative to the site root.
47
     *
48
     * @var string
49
     */
50
    const ROOT = 'ROOT';
51
52
    /**
53
     * specifies this url is relative to the current request.
54
     *
55
     * @var string
56
     */
57
    const REQUEST = 'REQUEST';
58
59
    /**
60
     * @config
61
     * @var array
62
     */
63
    private static $rules = array();
0 ignored issues
show
introduced by
The private property $rules is not used, and could be removed.
Loading history...
64
65
    /**
66
     * Set current page
67
     *
68
     * @internal
69
     * @var SiteTree
70
     */
71
    private static $current_page;
72
73
    /**
74
     * @config
75
     * @var string
76
     */
77
    private static $alternate_base_folder;
0 ignored issues
show
introduced by
The private property $alternate_base_folder is not used, and could be removed.
Loading history...
78
79
    /**
80
     * Override PUBLIC_DIR. Set to a non-null value to override.
81
     * Setting to an empty string will disable public dir.
82
     *
83
     * @config
84
     * @var bool|null
85
     */
86
    private static $alternate_public_dir = null;
0 ignored issues
show
introduced by
The private property $alternate_public_dir is not used, and could be removed.
Loading history...
87
88
    /**
89
     * Base url to populate if cannot be determined otherwise.
90
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
91
     *
92
     * @config
93
     * @var string
94
     */
95
    private static $default_base_url = '`SS_BASE_URL`';
0 ignored issues
show
introduced by
The private property $default_base_url is not used, and could be removed.
Loading history...
96
97
    public function __construct()
98
    {
99
    }
100
101
    /**
102
     * Test a URL request, returning a response object. This method is a wrapper around
103
     * Director::handleRequest() to assist with functional testing. It will execute the URL given, and
104
     * return the result as an HTTPResponse object.
105
     *
106
     * @param string $url The URL to visit.
107
     * @param array $postVars The $_POST & $_FILES variables.
108
     * @param array|Session $session The {@link Session} object representing the current session.
109
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
110
     * session.
111
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
112
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
113
     * @param string $body The HTTP body.
114
     * @param array $headers HTTP headers with key-value pairs.
115
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
116
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
117
     *
118
     * @return HTTPResponse
119
     *
120
     * @throws HTTPResponse_Exception
121
     */
122
    public static function test(
123
        $url,
124
        $postVars = [],
125
        $session = array(),
126
        $httpMethod = null,
127
        $body = null,
128
        $headers = array(),
129
        $cookies = array(),
130
        &$request = null
131
    ) {
132
        return static::mockRequest(
133
            function (HTTPRequest $request) {
134
                return Director::singleton()->handleRequest($request);
135
            },
136
            $url,
137
            $postVars,
138
            $session,
139
            $httpMethod,
140
            $body,
141
            $headers,
142
            $cookies,
143
            $request
144
        );
145
    }
146
147
    /**
148
     * Mock a request, passing this to the given callback, before resetting.
149
     *
150
     * @param callable $callback Action to pass the HTTPRequst object
151
     * @param string $url The URL to build
152
     * @param array $postVars The $_POST & $_FILES variables.
153
     * @param array|Session $session The {@link Session} object representing the current session.
154
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
155
     * session.
156
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
157
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
158
     * @param string $body The HTTP body.
159
     * @param array $headers HTTP headers with key-value pairs.
160
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
161
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
162
     * @return mixed Result of callback
163
     */
164
    public static function mockRequest(
165
        $callback,
166
        $url,
167
        $postVars = [],
168
        $session = [],
169
        $httpMethod = null,
170
        $body = null,
171
        $headers = [],
172
        $cookies = [],
173
        &$request = null
174
    ) {
175
        // Build list of cleanup promises
176
        $finally = [];
177
178
        /** @var Kernel $kernel */
179
        $kernel = Injector::inst()->get(Kernel::class);
180
        $kernel->nest();
181
        $finally[] = function () use ($kernel) {
182
            $kernel->activate();
183
        };
184
185
        // backup existing vars, and create new vars
186
        $existingVars = Environment::getVariables();
187
        $finally[] = function () use ($existingVars) {
188
            Environment::setVariables($existingVars);
189
        };
190
        $newVars = $existingVars;
191
192
        // These are needed so that calling Director::test() does not muck with whoever is calling it.
193
        // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
194
        if (class_exists(Versioned::class)) {
195
            $oldReadingMode = Versioned::get_reading_mode();
196
            $finally[] = function () use ($oldReadingMode) {
197
                Versioned::set_reading_mode($oldReadingMode);
198
            };
199
        }
200
201
        // Default httpMethod
202
        $newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod ?: ($postVars ? "POST" : "GET");
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
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
428
     * evaluated relative to the current base_url.
429
     *
430
     * @param string $url URL To transform to absolute.
431
     * @param string $relativeParent Method to use for evaluating relative urls.
432
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
433
     * Defaults to BASE, which is the same behaviour as template url resolution.
434
     * Ignored if the url is absolute or site root.
435
     *
436
     * @return string
437
     */
438
    public static function absoluteURL($url, $relativeParent = self::BASE)
439
    {
440
        if (is_bool($relativeParent)) {
0 ignored issues
show
introduced by
The condition is_bool($relativeParent) is always false.
Loading history...
441
            // Deprecate old boolean second parameter
442
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
443
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
444
        }
445
446
        // Check if there is already a protocol given
447
        if (preg_match('/^http(s?):\/\//', $url)) {
448
            return $url;
449
        }
450
451
        // Absolute urls without protocol are added
452
        // E.g. //google.com -> http://google.com
453
        if (strpos($url, '//') === 0) {
454
            return self::protocol() . substr($url, 2);
455
        }
456
457
        // Determine method for mapping the parent to this relative url
458
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
459
            // Root relative urls always should be evaluated relative to the root
460
            $parent = self::protocolAndHost();
461
        } elseif ($relativeParent === self::REQUEST) {
462
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
463
            if (!isset($_SERVER['REQUEST_URI'])) {
464
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
465
            }
466
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
467
        } else {
468
            // Default to respecting site base_url
469
            $parent = self::absoluteBaseURL();
470
        }
471
472
        // Map empty urls to relative slash and join to base
473
        if (empty($url) || $url === '.' || $url === './') {
474
            $url = '/';
475
        }
476
        return Controller::join_links($parent, $url);
477
    }
478
479
    /**
480
     * Return only host (and optional port) part of a url
481
     *
482
     * @param string $url
483
     * @return string|null Hostname, and optional port, or null if not a valid host
484
     */
485
    protected static function parseHost($url)
486
    {
487
        // Get base hostname
488
        $host = parse_url($url, PHP_URL_HOST);
489
        if (!$host) {
490
            return null;
491
        }
492
493
        // Include port
494
        $port = parse_url($url, PHP_URL_PORT);
495
        if ($port) {
496
            $host .= ':' . $port;
497
        }
498
499
        return $host;
500
    }
501
502
    /**
503
     * Validate user and password in URL, disallowing slashes
504
     *
505
     * @param string $url
506
     * @return bool
507
     */
508
    protected static function validateUserAndPass($url)
509
    {
510
        $parsedURL = parse_url($url);
511
512
        // Validate user (disallow slashes)
513
        if (!empty($parsedURL['user']) && strstr($parsedURL['user'], '\\')) {
514
            return false;
515
        }
516
        if (!empty($parsedURL['pass']) && strstr($parsedURL['pass'], '\\')) {
517
            return false;
518
        }
519
520
        return true;
521
    }
522
523
    /**
524
     * A helper to determine the current hostname used to access the site.
525
     * The following are used to determine the host (in order)
526
     *  - Director.alternate_base_url (if it contains a domain name)
527
     *  - Trusted proxy headers
528
     *  - HTTP Host header
529
     *  - SS_BASE_URL env var
530
     *  - SERVER_NAME
531
     *  - gethostname()
532
     *
533
     * @param HTTPRequest $request
534
     * @return string Host name, including port (if present)
535
     */
536
    public static function host(HTTPRequest $request = null)
537
    {
538
        // Check if overridden by alternate_base_url
539
        if ($baseURL = self::config()->get('alternate_base_url')) {
540
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
541
            $host = static::parseHost($baseURL);
542
            if ($host) {
543
                return $host;
544
            }
545
        }
546
547
        $request = static::currentRequest($request);
548
        if ($request && ($host = $request->getHeader('Host'))) {
549
            return $host;
550
        }
551
552
        // Check given header
553
        if (isset($_SERVER['HTTP_HOST'])) {
554
            return $_SERVER['HTTP_HOST'];
555
        }
556
557
        // Check base url
558
        if ($baseURL = self::config()->uninherited('default_base_url')) {
559
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
560
            $host = static::parseHost($baseURL);
561
            if ($host) {
562
                return $host;
563
            }
564
        }
565
566
        // Fail over to server_name (least reliable)
567
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
568
    }
569
570
    /**
571
     * Return port used for the base URL.
572
     * Note, this will be null if not specified, in which case you should assume the default
573
     * port for the current protocol.
574
     *
575
     * @param HTTPRequest $request
576
     * @return int|null
577
     */
578
    public static function port(HTTPRequest $request = null)
579
    {
580
        $host = static::host($request);
581
        return (int)parse_url($host, PHP_URL_PORT) ?: null;
582
    }
583
584
    /**
585
     * Return host name without port
586
     *
587
     * @param HTTPRequest|null $request
588
     * @return string|null
589
     */
590
    public static function hostName(HTTPRequest $request = null)
591
    {
592
        $host = static::host($request);
593
        return parse_url($host, PHP_URL_HOST) ?: null;
594
    }
595
596
    /**
597
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
598
     * variable isn't set.
599
     *
600
     * @param HTTPRequest $request
601
     * @return bool|string
602
     */
603
    public static function protocolAndHost(HTTPRequest $request = null)
604
    {
605
        return static::protocol($request) . static::host($request);
606
    }
607
608
    /**
609
     * Return the current protocol that the site is running under.
610
     *
611
     * @param HTTPRequest $request
612
     * @return string
613
     */
614
    public static function protocol(HTTPRequest $request = null)
615
    {
616
        return (self::is_https($request)) ? 'https://' : 'http://';
617
    }
618
619
    /**
620
     * Return whether the site is running as under HTTPS.
621
     *
622
     * @param HTTPRequest $request
623
     * @return bool
624
     */
625
    public static function is_https(HTTPRequest $request = null)
626
    {
627
        // Check override from alternate_base_url
628
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
629
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
630
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
631
            if ($protocol) {
632
                return $protocol === 'https';
633
            }
634
        }
635
636
        // Check the current request
637
        $request = static::currentRequest($request);
638
        if ($request && ($scheme = $request->getScheme())) {
639
            return $scheme === 'https';
640
        }
641
642
        // Check default_base_url
643
        if ($baseURL = self::config()->uninherited('default_base_url')) {
644
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
645
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
646
            if ($protocol) {
647
                return $protocol === 'https';
648
            }
649
        }
650
651
        return false;
652
    }
653
654
    /**
655
     * Return the root-relative url for the baseurl
656
     *
657
     * @return string Root-relative url with trailing slash.
658
     */
659
    public static function baseURL()
660
    {
661
        // Check override base_url
662
        $alternate = self::config()->get('alternate_base_url');
663
        if ($alternate) {
664
            $alternate = Injector::inst()->convertServiceProperty($alternate);
665
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
0 ignored issues
show
Bug introduced by
It seems like $alternate can also be of type array; 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

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

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