Passed
Push — master ( 9b9c6c...ef704e )
by Daniel
35:52 queued 24:20
created

src/Control/Director.php (1 issue)

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
            /** @var RequestHandler $controllerObj */
356
            $controllerObj = Injector::inst()->create($arguments['Controller']);
357
358
            // Handler for calling a controller
359
            $handler = function (HTTPRequest $request) use ($controllerObj) {
360
                try {
361
                    return $controllerObj->handleRequest($request);
362
                } catch (HTTPResponse_Exception $responseException) {
363
                    return $responseException->getResponse();
364
                }
365
            };
366
            break;
367
        }
368
369
        // Call the handler with the configured middlewares
370
        $response = $this->callMiddleware($request, $handler);
371
372
        // Note that if a different request was previously registered, this will now be lost
373
        // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
374
        Injector::inst()->unregisterNamedObject(HTTPRequest::class);
375
376
        return $response;
377
    }
378
379
    /**
380
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
381
     * object to return, then this will return the current controller.
382
     *
383
     * @return SiteTree|Controller
384
     */
385
    public static function get_current_page()
386
    {
387
        return self::$current_page ? self::$current_page : Controller::curr();
388
    }
389
390
    /**
391
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
392
     *
393
     * @param SiteTree $page
394
     */
395
    public static function set_current_page($page)
396
    {
397
        self::$current_page = $page;
398
    }
399
400
    /**
401
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
402
     * evaluated relative to the current base_url.
403
     *
404
     * @param string $url URL To transform to absolute.
405
     * @param string $relativeParent Method to use for evaluating relative urls.
406
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
407
     * Defaults to BASE, which is the same behaviour as template url resolution.
408
     * Ignored if the url is absolute or site root.
409
     *
410
     * @return string
411
     */
412
    public static function absoluteURL($url, $relativeParent = self::BASE)
413
    {
414
        if (is_bool($relativeParent)) {
415
            // Deprecate old boolean second parameter
416
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
417
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
418
        }
419
420
        // Check if there is already a protocol given
421
        if (preg_match('/^http(s?):\/\//', $url)) {
422
            return $url;
423
        }
424
425
        // Absolute urls without protocol are added
426
        // E.g. //google.com -> http://google.com
427
        if (strpos($url, '//') === 0) {
428
            return self::protocol() . substr($url, 2);
429
        }
430
431
        // Determine method for mapping the parent to this relative url
432
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
433
            // Root relative urls always should be evaluated relative to the root
434
            $parent = self::protocolAndHost();
435
        } elseif ($relativeParent === self::REQUEST) {
436
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
437
            if (!isset($_SERVER['REQUEST_URI'])) {
438
                return false;
439
            }
440
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
441
        } else {
442
            // Default to respecting site base_url
443
            $parent = self::absoluteBaseURL();
444
        }
445
446
        // Map empty urls to relative slash and join to base
447
        if (empty($url) || $url === '.' || $url === './') {
448
            $url = '/';
449
        }
450
        return Controller::join_links($parent, $url);
451
    }
452
453
    /**
454
     * Return only host (and optional port) part of a url
455
     *
456
     * @param string $url
457
     * @return string|null Hostname, and optional port, or null if not a valid host
458
     */
459
    protected static function parseHost($url)
460
    {
461
        // Get base hostname
462
        $host = parse_url($url, PHP_URL_HOST);
463
        if (!$host) {
464
            return null;
465
        }
466
467
        // Include port
468
        $port = parse_url($url, PHP_URL_PORT);
469
        if ($port) {
470
            $host .= ':' . $port;
471
        }
472
473
        return $host;
474
    }
475
476
    /**
477
     * A helper to determine the current hostname used to access the site.
478
     * The following are used to determine the host (in order)
479
     *  - Director.alternate_base_url (if it contains a domain name)
480
     *  - Trusted proxy headers
481
     *  - HTTP Host header
482
     *  - SS_BASE_URL env var
483
     *  - SERVER_NAME
484
     *  - gethostname()
485
     *
486
     * @param HTTPRequest $request
487
     * @return string Host name, including port (if present)
488
     */
489
    public static function host(HTTPRequest $request = null)
490
    {
491
        // Check if overridden by alternate_base_url
492
        if ($baseURL = self::config()->get('alternate_base_url')) {
493
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
494
            $host = static::parseHost($baseURL);
495
            if ($host) {
496
                return $host;
497
            }
498
        }
499
500
        $request = static::currentRequest($request);
501
        if ($request && ($host = $request->getHeader('Host'))) {
502
            return $host;
503
        }
504
505
        // Check given header
506
        if (isset($_SERVER['HTTP_HOST'])) {
507
            return $_SERVER['HTTP_HOST'];
508
        }
509
510
        // Check base url
511
        if ($baseURL = self::config()->uninherited('default_base_url')) {
512
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
513
            $host = static::parseHost($baseURL);
514
            if ($host) {
515
                return $host;
516
            }
517
        }
518
519
        // Fail over to server_name (least reliable)
520
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
521
    }
522
523
    /**
524
     * Return port used for the base URL.
525
     * Note, this will be null if not specified, in which case you should assume the default
526
     * port for the current protocol.
527
     *
528
     * @param HTTPRequest $request
529
     * @return int|null
530
     */
531
    public static function port(HTTPRequest $request = null)
532
    {
533
        $host = static::host($request);
534
        return (int)parse_url($host, PHP_URL_PORT) ?: null;
535
    }
536
537
    /**
538
     * Return host name without port
539
     *
540
     * @param HTTPRequest|null $request
541
     * @return string|null
542
     */
543
    public static function hostName(HTTPRequest $request = null)
544
    {
545
        $host = static::host($request);
546
        return parse_url($host, PHP_URL_HOST) ?: null;
547
    }
548
549
    /**
550
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
551
     * variable isn't set.
552
     *
553
     * @param HTTPRequest $request
554
     * @return bool|string
555
     */
556
    public static function protocolAndHost(HTTPRequest $request = null)
557
    {
558
        return static::protocol($request) . static::host($request);
559
    }
560
561
    /**
562
     * Return the current protocol that the site is running under.
563
     *
564
     * @param HTTPRequest $request
565
     * @return string
566
     */
567
    public static function protocol(HTTPRequest $request = null)
568
    {
569
        return (self::is_https($request)) ? 'https://' : 'http://';
570
    }
571
572
    /**
573
     * Return whether the site is running as under HTTPS.
574
     *
575
     * @param HTTPRequest $request
576
     * @return bool
577
     */
578
    public static function is_https(HTTPRequest $request = null)
579
    {
580
        // Check override from alternate_base_url
581
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
582
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
583
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
584
            if ($protocol) {
585
                return $protocol === 'https';
586
            }
587
        }
588
589
        // Check the current request
590
        $request = static::currentRequest($request);
591
        if ($request && ($scheme = $request->getScheme())) {
592
            return $scheme === 'https';
593
        }
594
595
        // Check default_base_url
596
        if ($baseURL = self::config()->uninherited('default_base_url')) {
597
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
598
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
599
            if ($protocol) {
600
                return $protocol === 'https';
601
            }
602
        }
603
604
        return false;
605
    }
606
607
    /**
608
     * Return the root-relative url for the baseurl
609
     *
610
     * @return string Root-relative url with trailing slash.
611
     */
612
    public static function baseURL()
613
    {
614
        // Check override base_url
615
        $alternate = self::config()->get('alternate_base_url');
616
        if ($alternate) {
617
            $alternate = Injector::inst()->convertServiceProperty($alternate);
618
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
619
        }
620
621
        // Get env base url
622
        $baseURL = rtrim(BASE_URL, '/') . '/';
623
624
        // Check if BASE_SCRIPT_URL is defined
625
        // e.g. `index.php/`
626
        if (defined('BASE_SCRIPT_URL')) {
627
            return $baseURL . BASE_SCRIPT_URL;
628
        }
629
630
        return $baseURL;
631
    }
632
633
    /**
634
     * Returns the root filesystem folder for the site. It will be automatically calculated unless
635
     * it is overridden with {@link setBaseFolder()}.
636
     *
637
     * @return string
638
     */
639
    public static function baseFolder()
640
    {
641
        $alternate = Director::config()->uninherited('alternate_base_folder');
642
        return $alternate ?: BASE_PATH;
643
    }
644
645
    /**
646
     * Check if using a seperate public dir, and if so return this directory
647
     * name.
648
     *
649
     * This will be removed in 5.0 and fixed to 'public'
650
     *
651
     * @return string
652
     */
653
    public static function publicDir()
654
    {
655
        $alternate = self::config()->uninherited('alternate_public_dir');
656
        if (isset($alternate)) {
657
            return $alternate;
658
        }
659
        return PUBLIC_DIR;
660
    }
661
662
    /**
663
     * Gets the webroot of the project, which may be a subfolder of {@see baseFolder()}
664
     *
665
     * @return string
666
     */
667
    public static function publicFolder()
668
    {
669
        $folder = self::baseFolder();
670
        $publicDir = self::publicDir();
671
        if ($publicDir) {
672
            return Path::join($folder, $publicDir);
673
        }
674
675
        return $folder;
676
    }
677
678
    /**
679
     * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
680
     * when turning a URL into a filesystem reference, or vice versa.
681
     *
682
     * Note: You should check {@link Director::is_site_url()} if making an untrusted url relative prior
683
     * to calling this function.
684
     *
685
     * @param string $url Accepts both a URL or a filesystem path.
686
     * @return string
687
     */
688
    public static function makeRelative($url)
689
    {
690
        // Allow for the accidental inclusion whitespace and // in the URL
691
        $url = preg_replace('#([^:])//#', '\\1/', trim($url));
692
693
        // If using a real url, remove protocol / hostname / auth / port
694
        if (preg_match('#^(?<protocol>https?:)?//(?<hostpart>[^/]*)(?<url>(/.*)?)$#i', $url, $matches)) {
695
            $url = $matches['url'];
696
        }
697
698
        // Empty case
699
        if (trim($url, '\\/') === '') {
700
            return '';
701
        }
702
703
        // Remove base folder or url
704
        foreach ([self::publicFolder(), self::baseFolder(), self::baseURL()] as $base) {
705
            // Ensure single / doesn't break comparison (unless it would make base empty)
706
            $base = rtrim($base, '\\/') ?: $base;
707
            if (stripos($url, $base) === 0) {
708
                return ltrim(substr($url, strlen($base)), '\\/');
709
            }
710
        }
711
712
        // Nothing matched, fall back to returning the original URL
713
        return $url;
714
    }
715
716
    /**
717
     * Returns true if a given path is absolute. Works under both *nix and windows systems.
718
     *
719
     * @param string $path
720
     *
721
     * @return bool
722
     */
723
    public static function is_absolute($path)
724
    {
725
        if (empty($path)) {
726
            return false;
727
        }
728
        if ($path[0] == '/' || $path[0] == '\\') {
729
            return true;
730
        }
731
        return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
732
    }
733
734
    /**
735
     * Determine if the url is root relative (i.e. starts with /, but not with //) SilverStripe
736
     * considers root relative urls as a subset of relative urls.
737
     *
738
     * @param string $url
739
     *
740
     * @return bool
741
     */
742
    public static function is_root_relative_url($url)
743
    {
744
        return strpos($url, '/') === 0 && strpos($url, '//') !== 0;
745
    }
746
747
    /**
748
     * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). URLs beginning with "//"
749
     * are treated as absolute, as browsers take this to mean the same protocol as currently being used.
750
     *
751
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
752
     * and avoid phishing attacks by redirecting to an attackers server.
753
     *
754
     * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
755
     * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
756
     *
757
     * @param string $url
758
     *
759
     * @return bool
760
     */
761
    public static function is_absolute_url($url)
762
    {
763
        // Strip off the query and fragment parts of the URL before checking
764
        if (($queryPosition = strpos($url, '?')) !== false) {
765
            $url = substr($url, 0, $queryPosition - 1);
766
        }
767
        if (($hashPosition = strpos($url, '#')) !== false) {
768
            $url = substr($url, 0, $hashPosition - 1);
769
        }
770
        $colonPosition = strpos($url, ':');
771
        $slashPosition = strpos($url, '/');
772
        return (
773
            // Base check for existence of a host on a compliant URL
774
            parse_url($url, PHP_URL_HOST)
775
            // Check for more than one leading slash without a protocol.
776
            // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
777
            // and hence a potential security risk. Single leading slashes are not an issue though.
778
            || preg_match('%^\s*/{2,}%', $url)
779
            || (
780
                // If a colon is found, check if it's part of a valid scheme definition
781
                // (meaning its not preceded by a slash).
782
                $colonPosition !== false
783
                && ($slashPosition === false || $colonPosition < $slashPosition)
784
            )
785
        );
786
    }
787
788
    /**
789
     * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()}.
790
     *
791
     * @param string $url
792
     *
793
     * @return bool
794
     */
795
    public static function is_relative_url($url)
796
    {
797
        return !static::is_absolute_url($url);
798
    }
799
800
    /**
801
     * Checks if the given URL is belonging to this "site" (not an external link). That's the case if
802
     * the URL is relative, as defined by {@link is_relative_url()}, or if the host matches
803
     * {@link protocolAndHost()}.
804
     *
805
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
806
     * and avoid phishing attacks by redirecting to an attackers server.
807
     *
808
     * @param string $url
809
     *
810
     * @return bool
811
     */
812
    public static function is_site_url($url)
813
    {
814
        // Validate host[:port]
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
815
        $urlHost = static::parseHost($url);
816
        if ($urlHost && $urlHost === static::host()) {
817
            return true;
818
        }
819
820
        // Relative urls always are site urls
821
        return self::is_relative_url($url);
822
    }
823
824
    /**
825
     * Given a filesystem reference relative to the site root, return the full file-system path.
826
     *
827
     * @param string $file
828
     *
829
     * @return string
830
     */
831
    public static function getAbsFile($file)
832
    {
833
        // If already absolute
834
        if (self::is_absolute($file)) {
835
            return $file;
836
        }
837
838
        // If path is relative to public folder search there first
839
        if (self::publicDir()) {
840
            $path = Path::join(self::publicFolder(), $file);
841
            if (file_exists($path)) {
842
                return $path;
843
            }
844
        }
845
846
        // Default to base folder
847
        return Path::join(self::baseFolder(), $file);
848
    }
849
850
    /**
851
     * Returns true if the given file exists. Filename should be relative to the site root.
852
     *
853
     * @param $file
854
     *
855
     * @return bool
856
     */
857
    public static function fileExists($file)
858
    {
859
        // replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
860
        $file = preg_replace('/([^\?]*)?.*/', '$1', $file);
861
        return file_exists(Director::getAbsFile($file));
862
    }
863
864
    /**
865
     * Returns the Absolute URL of the site root.
866
     *
867
     * @return string
868
     */
869
    public static function absoluteBaseURL()
870
    {
871
        return self::absoluteURL(
872
            self::baseURL(),
873
            self::ROOT
874
        );
875
    }
876
877
    /**
878
     * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
879
     * the URL.
880
     *
881
     * @param HTTPRequest|null $request
882
     * @return string
883
     */
884
    public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
885
    {
886
        // Detect basic auth
887
        $user = $request->getHeader('PHP_AUTH_USER');
888
        if ($user) {
889
            $password = $request->getHeader('PHP_AUTH_PW');
890
            $login = sprintf("%s:%s@", $user, $password) ;
891
        } else {
892
            $login = '';
893
        }
894
895
        return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
896
    }
897
898
    /**
899
     * Skip any further processing and immediately respond with a redirect to the passed URL.
900
     *
901
     * @param string $destURL
902
     * @throws HTTPResponse_Exception
903
     */
904
    protected static function force_redirect($destURL)
905
    {
906
        // Redirect to installer
907
        $response = new HTTPResponse();
908
        $response->redirect($destURL, 301);
909
        HTTP::add_cache_headers($response);
910
        throw new HTTPResponse_Exception($response);
911
    }
912
913
    /**
914
     * Force the site to run on SSL.
915
     *
916
     * To use, call from the init() method of your PageController. For example:
917
     * <code>
918
     * if (Director::isLive()) Director::forceSSL();
919
     * </code>
920
     *
921
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
922
     * patterns for matching relative URLs. For example:
923
     * <code>
924
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
925
     * </code>
926
     *
927
     * If you want certain parts of your site protected under a different domain, you can specify
928
     * the domain as an argument:
929
     * <code>
930
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
931
     * </code>
932
     *
933
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
934
     * to ensure that this won't cause usability problems.
935
     *
936
     * CAUTION: This does not respect the site environment mode. You should check this
937
     * as per the above examples using Director::isLive() or Director::isTest() for example.
938
     *
939
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
940
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
941
     * Can include port number.
942
     * @param HTTPRequest|null $request Request object to check
943
     */
944
    public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
945
    {
946
        $handler = CanonicalURLMiddleware::singleton()->setForceSSL(true);
947
        if ($patterns) {
948
            $handler->setForceSSLPatterns($patterns);
949
        }
950
        if ($secureDomain) {
951
            $handler->setForceSSLDomain($secureDomain);
952
        }
953
        $handler->throwRedirectIfNeeded($request);
954
    }
955
956
    /**
957
     * Force a redirect to a domain starting with "www."
958
     *
959
     * @param HTTPRequest $request
960
     */
961
    public static function forceWWW(HTTPRequest $request = null)
962
    {
963
        $handler = CanonicalURLMiddleware::singleton()->setForceWWW(true);
964
        $handler->throwRedirectIfNeeded($request);
965
    }
966
967
    /**
968
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
969
     * jQuery or whether a manually set request-parameter 'ajax' is present.
970
     *
971
     * @param HTTPRequest $request
972
     * @return bool
973
     */
974
    public static function is_ajax(HTTPRequest $request = null)
975
    {
976
        $request = self::currentRequest($request);
977
        if ($request) {
978
            return $request->isAjax();
979
        } else {
980
            return (
981
                isset($_REQUEST['ajax']) ||
982
                (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
983
            );
984
        }
985
    }
986
987
    /**
988
     * Returns true if this script is being run from the command line rather than the web server.
989
     *
990
     * @return bool
991
     */
992
    public static function is_cli()
993
    {
994
        return in_array(php_sapi_name(), ['cli', 'phpdbg']);
995
    }
996
997
    /**
998
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
999
     * {@link Director::isLive()}.
1000
     *
1001
     * @return string
1002
     */
1003
    public static function get_environment_type()
1004
    {
1005
        /** @var Kernel $kernel */
1006
        $kernel = Injector::inst()->get(Kernel::class);
1007
        return $kernel->getEnvironment();
1008
    }
1009
1010
    /**
1011
     * This function will return true if the site is in a live environment. For information about
1012
     * environment types, see {@link Director::set_environment_type()}.
1013
     *
1014
     * @return bool
1015
     */
1016
    public static function isLive()
1017
    {
1018
        return self::get_environment_type() === 'live';
1019
    }
1020
1021
    /**
1022
     * This function will return true if the site is in a development environment. For information about
1023
     * environment types, see {@link Director::set_environment_type()}.
1024
     *
1025
     * @return bool
1026
     */
1027
    public static function isDev()
1028
    {
1029
        return self::get_environment_type() === 'dev';
1030
    }
1031
1032
    /**
1033
     * This function will return true if the site is in a test environment. For information about
1034
     * environment types, see {@link Director::set_environment_type()}.
1035
     *
1036
     * @return bool
1037
     */
1038
    public static function isTest()
1039
    {
1040
        return self::get_environment_type() === 'test';
1041
    }
1042
1043
    /**
1044
     * Returns an array of strings of the method names of methods on the call that should be exposed
1045
     * as global variables in the templates.
1046
     *
1047
     * @return array
1048
     */
1049
    public static function get_template_global_variables()
1050
    {
1051
        return array(
1052
            'absoluteBaseURL',
1053
            'baseURL',
1054
            'is_ajax',
1055
            'isAjax' => 'is_ajax',
1056
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1057
        );
1058
    }
1059
1060
    /**
1061
     * Helper to validate or check the current request object
1062
     *
1063
     * @param HTTPRequest $request
1064
     * @return HTTPRequest Request object if one is both current and valid
1065
     */
1066
    protected static function currentRequest(HTTPRequest $request = null)
1067
    {
1068
        // Ensure we only use a registered HTTPRequest and don't
1069
        // incidentally construct a singleton
1070
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
1071
            $request = Injector::inst()->get(HTTPRequest::class);
1072
        }
1073
        return $request;
1074
    }
1075
}
1076