Completed
Push — master ( 32a670...32a37c )
by Damian
37s queued 21s
created

src/Control/Director.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\CMS\Model\SiteTree;
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();
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;
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;
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`';
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) {
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) {
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
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
380
     * object to return, then this will return the current controller.
381
     *
382
     * @return SiteTree|Controller
383
     */
384
    public static function get_current_page()
385
    {
386
        return self::$current_page ? self::$current_page : Controller::curr();
387
    }
388
389
    /**
390
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
391
     *
392
     * @param SiteTree $page
393
     */
394
    public static function set_current_page($page)
395
    {
396
        self::$current_page = $page;
397
    }
398
399
    /**
400
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
401
     * evaluated relative to the current base_url.
402
     *
403
     * @param string $url URL To transform to absolute.
404
     * @param string $relativeParent Method to use for evaluating relative urls.
405
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
406
     * Defaults to BASE, which is the same behaviour as template url resolution.
407
     * Ignored if the url is absolute or site root.
408
     *
409
     * @return string
410
     */
411
    public static function absoluteURL($url, $relativeParent = self::BASE)
412
    {
413
        if (is_bool($relativeParent)) {
414
            // Deprecate old boolean second parameter
415
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
416
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
417
        }
418
419
        // Check if there is already a protocol given
420
        if (preg_match('/^http(s?):\/\//', $url)) {
421
            return $url;
422
        }
423
424
        // Absolute urls without protocol are added
425
        // E.g. //google.com -> http://google.com
426
        if (strpos($url, '//') === 0) {
427
            return self::protocol() . substr($url, 2);
428
        }
429
430
        // Determine method for mapping the parent to this relative url
431
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
432
            // Root relative urls always should be evaluated relative to the root
433
            $parent = self::protocolAndHost();
434
        } elseif ($relativeParent === self::REQUEST) {
435
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
436
            if (!isset($_SERVER['REQUEST_URI'])) {
437
                return false;
438
            }
439
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
440
        } else {
441
            // Default to respecting site base_url
442
            $parent = self::absoluteBaseURL();
443
        }
444
445
        // Map empty urls to relative slash and join to base
446
        if (empty($url) || $url === '.' || $url === './') {
447
            $url = '/';
448
        }
449
        return Controller::join_links($parent, $url);
450
    }
451
452
    /**
453
     * Return only host (and optional port) part of a url
454
     *
455
     * @param string $url
456
     * @return string|null Hostname, and optional port, or null if not a valid host
457
     */
458
    protected static function parseHost($url)
459
    {
460
        // Get base hostname
461
        $host = parse_url($url, PHP_URL_HOST);
462
        if (!$host) {
463
            return null;
464
        }
465
466
        // Include port
467
        $port = parse_url($url, PHP_URL_PORT);
468
        if ($port) {
469
            $host .= ':' . $port;
470
        }
471
472
        return $host;
473
    }
474
475
    /**
476
     * Validate user and password in URL, disallowing slashes
477
     *
478
     * @param string $url
479
     * @return bool
480
     */
481
    protected static function validateUserAndPass($url)
482
    {
483
        $parsedURL = parse_url($url);
484
485
        // Validate user (disallow slashes)
486
        if (!empty($parsedURL['user']) && strstr($parsedURL['user'], '\\')) {
487
            return false;
488
        }
489
        if (!empty($parsedURL['pass']) && strstr($parsedURL['pass'], '\\')) {
490
            return false;
491
        }
492
493
        return true;
494
    }
495
496
    /**
497
     * A helper to determine the current hostname used to access the site.
498
     * The following are used to determine the host (in order)
499
     *  - Director.alternate_base_url (if it contains a domain name)
500
     *  - Trusted proxy headers
501
     *  - HTTP Host header
502
     *  - SS_BASE_URL env var
503
     *  - SERVER_NAME
504
     *  - gethostname()
505
     *
506
     * @param HTTPRequest $request
507
     * @return string Host name, including port (if present)
508
     */
509
    public static function host(HTTPRequest $request = null)
510
    {
511
        // Check if overridden by alternate_base_url
512
        if ($baseURL = self::config()->get('alternate_base_url')) {
513
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
514
            $host = static::parseHost($baseURL);
515
            if ($host) {
516
                return $host;
517
            }
518
        }
519
520
        $request = static::currentRequest($request);
521
        if ($request && ($host = $request->getHeader('Host'))) {
522
            return $host;
523
        }
524
525
        // Check given header
526
        if (isset($_SERVER['HTTP_HOST'])) {
527
            return $_SERVER['HTTP_HOST'];
528
        }
529
530
        // Check base url
531
        if ($baseURL = self::config()->uninherited('default_base_url')) {
532
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
533
            $host = static::parseHost($baseURL);
534
            if ($host) {
535
                return $host;
536
            }
537
        }
538
539
        // Fail over to server_name (least reliable)
540
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
541
    }
542
543
    /**
544
     * Return port used for the base URL.
545
     * Note, this will be null if not specified, in which case you should assume the default
546
     * port for the current protocol.
547
     *
548
     * @param HTTPRequest $request
549
     * @return int|null
550
     */
551
    public static function port(HTTPRequest $request = null)
552
    {
553
        $host = static::host($request);
554
        return (int)parse_url($host, PHP_URL_PORT) ?: null;
555
    }
556
557
    /**
558
     * Return host name without port
559
     *
560
     * @param HTTPRequest|null $request
561
     * @return string|null
562
     */
563
    public static function hostName(HTTPRequest $request = null)
564
    {
565
        $host = static::host($request);
566
        return parse_url($host, PHP_URL_HOST) ?: null;
567
    }
568
569
    /**
570
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
571
     * variable isn't set.
572
     *
573
     * @param HTTPRequest $request
574
     * @return bool|string
575
     */
576
    public static function protocolAndHost(HTTPRequest $request = null)
577
    {
578
        return static::protocol($request) . static::host($request);
579
    }
580
581
    /**
582
     * Return the current protocol that the site is running under.
583
     *
584
     * @param HTTPRequest $request
585
     * @return string
586
     */
587
    public static function protocol(HTTPRequest $request = null)
588
    {
589
        return (self::is_https($request)) ? 'https://' : 'http://';
590
    }
591
592
    /**
593
     * Return whether the site is running as under HTTPS.
594
     *
595
     * @param HTTPRequest $request
596
     * @return bool
597
     */
598
    public static function is_https(HTTPRequest $request = null)
599
    {
600
        // Check override from alternate_base_url
601
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
602
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
603
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
604
            if ($protocol) {
605
                return $protocol === 'https';
606
            }
607
        }
608
609
        // Check the current request
610
        $request = static::currentRequest($request);
611
        if ($request && ($scheme = $request->getScheme())) {
612
            return $scheme === 'https';
613
        }
614
615
        // Check default_base_url
616
        if ($baseURL = self::config()->uninherited('default_base_url')) {
617
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
618
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
619
            if ($protocol) {
620
                return $protocol === 'https';
621
            }
622
        }
623
624
        return false;
625
    }
626
627
    /**
628
     * Return the root-relative url for the baseurl
629
     *
630
     * @return string Root-relative url with trailing slash.
631
     */
632
    public static function baseURL()
633
    {
634
        // Check override base_url
635
        $alternate = self::config()->get('alternate_base_url');
636
        if ($alternate) {
637
            $alternate = Injector::inst()->convertServiceProperty($alternate);
638
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
0 ignored issues
show
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

638
            return rtrim(parse_url(/** @scrutinizer ignore-type */ $alternate, PHP_URL_PATH), '/') . '/';
Loading history...
639
        }
640
641
        // Get env base url
642
        $baseURL = rtrim(BASE_URL, '/') . '/';
643
644
        // Check if BASE_SCRIPT_URL is defined
645
        // e.g. `index.php/`
646
        if (defined('BASE_SCRIPT_URL')) {
647
            return $baseURL . BASE_SCRIPT_URL;
648
        }
649
650
        return $baseURL;
651
    }
652
653
    /**
654
     * Returns the root filesystem folder for the site. It will be automatically calculated unless
655
     * it is overridden with {@link setBaseFolder()}.
656
     *
657
     * @return string
658
     */
659
    public static function baseFolder()
660
    {
661
        $alternate = Director::config()->uninherited('alternate_base_folder');
662
        return $alternate ?: BASE_PATH;
663
    }
664
665
    /**
666
     * Check if using a seperate public dir, and if so return this directory
667
     * name.
668
     *
669
     * This will be removed in 5.0 and fixed to 'public'
670
     *
671
     * @return string
672
     */
673
    public static function publicDir()
674
    {
675
        $alternate = self::config()->uninherited('alternate_public_dir');
676
        if (isset($alternate)) {
677
            return $alternate;
678
        }
679
        return PUBLIC_DIR;
680
    }
681
682
    /**
683
     * Gets the webroot of the project, which may be a subfolder of {@see baseFolder()}
684
     *
685
     * @return string
686
     */
687
    public static function publicFolder()
688
    {
689
        $folder = self::baseFolder();
690
        $publicDir = self::publicDir();
691
        if ($publicDir) {
692
            return Path::join($folder, $publicDir);
693
        }
694
695
        return $folder;
696
    }
697
698
    /**
699
     * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
700
     * when turning a URL into a filesystem reference, or vice versa.
701
     *
702
     * Note: You should check {@link Director::is_site_url()} if making an untrusted url relative prior
703
     * to calling this function.
704
     *
705
     * @param string $url Accepts both a URL or a filesystem path.
706
     * @return string
707
     */
708
    public static function makeRelative($url)
709
    {
710
        // Allow for the accidental inclusion whitespace and // in the URL
711
        $url = preg_replace('#([^:])//#', '\\1/', trim($url));
712
713
        // If using a real url, remove protocol / hostname / auth / port
714
        if (preg_match('#^(?<protocol>https?:)?//(?<hostpart>[^/]*)(?<url>(/.*)?)$#i', $url, $matches)) {
715
            $url = $matches['url'];
716
        }
717
718
        // Empty case
719
        if (trim($url, '\\/') === '') {
720
            return '';
721
        }
722
723
        // Remove base folder or url
724
        foreach ([self::publicFolder(), self::baseFolder(), self::baseURL()] as $base) {
725
            // Ensure single / doesn't break comparison (unless it would make base empty)
726
            $base = rtrim($base, '\\/') ?: $base;
727
            if (stripos($url, $base) === 0) {
728
                return ltrim(substr($url, strlen($base)), '\\/');
729
            }
730
        }
731
732
        // Nothing matched, fall back to returning the original URL
733
        return $url;
734
    }
735
736
    /**
737
     * Returns true if a given path is absolute. Works under both *nix and windows systems.
738
     *
739
     * @param string $path
740
     *
741
     * @return bool
742
     */
743
    public static function is_absolute($path)
744
    {
745
        if (empty($path)) {
746
            return false;
747
        }
748
        if ($path[0] == '/' || $path[0] == '\\') {
749
            return true;
750
        }
751
        return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
752
    }
753
754
    /**
755
     * Determine if the url is root relative (i.e. starts with /, but not with //) SilverStripe
756
     * considers root relative urls as a subset of relative urls.
757
     *
758
     * @param string $url
759
     *
760
     * @return bool
761
     */
762
    public static function is_root_relative_url($url)
763
    {
764
        return strpos($url, '/') === 0 && strpos($url, '//') !== 0;
765
    }
766
767
    /**
768
     * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). URLs beginning with "//"
769
     * are treated as absolute, as browsers take this to mean the same protocol as currently being used.
770
     *
771
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
772
     * and avoid phishing attacks by redirecting to an attackers server.
773
     *
774
     * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
775
     * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
776
     *
777
     * @param string $url
778
     *
779
     * @return bool
780
     */
781
    public static function is_absolute_url($url)
782
    {
783
        // Strip off the query and fragment parts of the URL before checking
784
        if (($queryPosition = strpos($url, '?')) !== false) {
785
            $url = substr($url, 0, $queryPosition - 1);
786
        }
787
        if (($hashPosition = strpos($url, '#')) !== false) {
788
            $url = substr($url, 0, $hashPosition - 1);
789
        }
790
        $colonPosition = strpos($url, ':');
791
        $slashPosition = strpos($url, '/');
792
        return (
793
            // Base check for existence of a host on a compliant URL
794
            parse_url($url, PHP_URL_HOST)
795
            // Check for more than one leading slash without a protocol.
796
            // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
797
            // and hence a potential security risk. Single leading slashes are not an issue though.
798
            || preg_match('%^\s*/{2,}%', $url)
799
            || (
800
                // If a colon is found, check if it's part of a valid scheme definition
801
                // (meaning its not preceded by a slash).
802
                $colonPosition !== false
803
                && ($slashPosition === false || $colonPosition < $slashPosition)
804
            )
805
        );
806
    }
807
808
    /**
809
     * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()}.
810
     *
811
     * @param string $url
812
     *
813
     * @return bool
814
     */
815
    public static function is_relative_url($url)
816
    {
817
        return !static::is_absolute_url($url);
818
    }
819
820
    /**
821
     * Checks if the given URL is belonging to this "site" (not an external link). That's the case if
822
     * the URL is relative, as defined by {@link is_relative_url()}, or if the host matches
823
     * {@link protocolAndHost()}.
824
     *
825
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
826
     * and avoid phishing attacks by redirecting to an attackers server.
827
     *
828
     * @param string $url
829
     *
830
     * @return bool
831
     */
832
    public static function is_site_url($url)
833
    {
834
        // Validate user and password
835
        if (!static::validateUserAndPass($url)) {
836
            return false;
837
        }
838
839
        // Validate host[:port]
840
        $urlHost = static::parseHost($url);
841
        if ($urlHost && $urlHost === static::host()) {
842
            return true;
843
        }
844
845
        // Relative urls always are site urls
846
        return self::is_relative_url($url);
847
    }
848
849
    /**
850
     * Given a filesystem reference relative to the site root, return the full file-system path.
851
     *
852
     * @param string $file
853
     *
854
     * @return string
855
     */
856
    public static function getAbsFile($file)
857
    {
858
        // If already absolute
859
        if (self::is_absolute($file)) {
860
            return $file;
861
        }
862
863
        // If path is relative to public folder search there first
864
        if (self::publicDir()) {
865
            $path = Path::join(self::publicFolder(), $file);
866
            if (file_exists($path)) {
867
                return $path;
868
            }
869
        }
870
871
        // Default to base folder
872
        return Path::join(self::baseFolder(), $file);
873
    }
874
875
    /**
876
     * Returns true if the given file exists. Filename should be relative to the site root.
877
     *
878
     * @param $file
879
     *
880
     * @return bool
881
     */
882
    public static function fileExists($file)
883
    {
884
        // replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
885
        $file = preg_replace('/([^\?]*)?.*/', '$1', $file);
886
        return file_exists(Director::getAbsFile($file));
887
    }
888
889
    /**
890
     * Returns the Absolute URL of the site root.
891
     *
892
     * @return string
893
     */
894
    public static function absoluteBaseURL()
895
    {
896
        return self::absoluteURL(
897
            self::baseURL(),
898
            self::ROOT
899
        );
900
    }
901
902
    /**
903
     * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
904
     * the URL.
905
     *
906
     * @param HTTPRequest|null $request
907
     * @return string
908
     */
909
    public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
910
    {
911
        // Detect basic auth
912
        $user = $request->getHeader('PHP_AUTH_USER');
913
        if ($user) {
914
            $password = $request->getHeader('PHP_AUTH_PW');
915
            $login = sprintf("%s:%s@", $user, $password) ;
916
        } else {
917
            $login = '';
918
        }
919
920
        return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
921
    }
922
923
    /**
924
     * Skip any further processing and immediately respond with a redirect to the passed URL.
925
     *
926
     * @param string $destURL
927
     * @throws HTTPResponse_Exception
928
     */
929
    protected static function force_redirect($destURL)
930
    {
931
        // Redirect to installer
932
        $response = new HTTPResponse();
933
        $response->redirect($destURL, 301);
934
        throw new HTTPResponse_Exception($response);
935
    }
936
937
    /**
938
     * Force the site to run on SSL.
939
     *
940
     * To use, call from the init() method of your PageController. For example:
941
     * <code>
942
     * if (Director::isLive()) Director::forceSSL();
943
     * </code>
944
     *
945
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
946
     * patterns for matching relative URLs. For example:
947
     * <code>
948
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
949
     * </code>
950
     *
951
     * If you want certain parts of your site protected under a different domain, you can specify
952
     * the domain as an argument:
953
     * <code>
954
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
955
     * </code>
956
     *
957
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
958
     * to ensure that this won't cause usability problems.
959
     *
960
     * CAUTION: This does not respect the site environment mode. You should check this
961
     * as per the above examples using Director::isLive() or Director::isTest() for example.
962
     *
963
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
964
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
965
     * Can include port number.
966
     * @param HTTPRequest|null $request Request object to check
967
     */
968
    public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
969
    {
970
        $handler = CanonicalURLMiddleware::singleton()->setForceSSL(true);
971
        if ($patterns) {
972
            $handler->setForceSSLPatterns($patterns);
973
        }
974
        if ($secureDomain) {
975
            $handler->setForceSSLDomain($secureDomain);
976
        }
977
        $handler->throwRedirectIfNeeded($request);
978
    }
979
980
    /**
981
     * Force a redirect to a domain starting with "www."
982
     *
983
     * @param HTTPRequest $request
984
     */
985
    public static function forceWWW(HTTPRequest $request = null)
986
    {
987
        $handler = CanonicalURLMiddleware::singleton()->setForceWWW(true);
988
        $handler->throwRedirectIfNeeded($request);
989
    }
990
991
    /**
992
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
993
     * jQuery or whether a manually set request-parameter 'ajax' is present.
994
     *
995
     * Note that if you plan to use this to alter your HTTP response on a cached page,
996
     * you should add X-Requested-With to the Vary header.
997
     *
998
     * @param HTTPRequest $request
999
     * @return bool
1000
     */
1001
    public static function is_ajax(HTTPRequest $request = null)
1002
    {
1003
        $request = self::currentRequest($request);
1004
        if ($request) {
1005
            return $request->isAjax();
1006
        }
1007
1008
        return (
1009
            isset($_REQUEST['ajax']) ||
1010
            (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
1011
        );
1012
    }
1013
1014
    /**
1015
     * Returns true if this script is being run from the command line rather than the web server.
1016
     *
1017
     * @return bool
1018
     */
1019
    public static function is_cli()
1020
    {
1021
        return in_array(php_sapi_name(), ['cli', 'phpdbg']);
1022
    }
1023
1024
    /**
1025
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
1026
     * {@link Director::isLive()}.
1027
     *
1028
     * @return string
1029
     */
1030
    public static function get_environment_type()
1031
    {
1032
        /** @var Kernel $kernel */
1033
        $kernel = Injector::inst()->get(Kernel::class);
1034
        return $kernel->getEnvironment();
1035
    }
1036
1037
    /**
1038
     * This function will return true if the site is in a live environment. For information about
1039
     * environment types, see {@link Director::set_environment_type()}.
1040
     *
1041
     * @return bool
1042
     */
1043
    public static function isLive()
1044
    {
1045
        return self::get_environment_type() === 'live';
1046
    }
1047
1048
    /**
1049
     * This function will return true if the site is in a development environment. For information about
1050
     * environment types, see {@link Director::set_environment_type()}.
1051
     *
1052
     * @return bool
1053
     */
1054
    public static function isDev()
1055
    {
1056
        return self::get_environment_type() === 'dev';
1057
    }
1058
1059
    /**
1060
     * This function will return true if the site is in a test environment. For information about
1061
     * environment types, see {@link Director::set_environment_type()}.
1062
     *
1063
     * @return bool
1064
     */
1065
    public static function isTest()
1066
    {
1067
        return self::get_environment_type() === 'test';
1068
    }
1069
1070
    /**
1071
     * Returns an array of strings of the method names of methods on the call that should be exposed
1072
     * as global variables in the templates.
1073
     *
1074
     * @return array
1075
     */
1076
    public static function get_template_global_variables()
1077
    {
1078
        return array(
1079
            'absoluteBaseURL',
1080
            'baseURL',
1081
            'is_ajax',
1082
            'isAjax' => 'is_ajax',
1083
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1084
        );
1085
    }
1086
1087
    /**
1088
     * Helper to validate or check the current request object
1089
     *
1090
     * @param HTTPRequest $request
1091
     * @return HTTPRequest Request object if one is both current and valid
1092
     */
1093
    protected static function currentRequest(HTTPRequest $request = null)
1094
    {
1095
        // Ensure we only use a registered HTTPRequest and don't
1096
        // incidentally construct a singleton
1097
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
1098
            $request = Injector::inst()->get(HTTPRequest::class);
1099
        }
1100
        return $request;
1101
    }
1102
}
1103