Completed
Push — master ( daed8c...cf758d )
by Damian
08:03
created

src/Control/Director.php (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\Core\Environment;
9
use SilverStripe\Core\Injector\Injectable;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Core\Kernel;
12
use SilverStripe\Dev\Deprecation;
13
use SilverStripe\Versioned\Versioned;
14
use SilverStripe\View\Requirements;
15
use SilverStripe\View\Requirements_Backend;
16
use SilverStripe\View\TemplateGlobalProvider;
17
18
/**
19
 * Director is responsible for processing URLs, and providing environment information.
20
 *
21
 * The most important part of director is {@link Director::handleRequest()}, which is passed an HTTPRequest and will
22
 * execute the appropriate controller.
23
 *
24
 * Director also has a number of static methods that provide information about the environment, such as
25
 * {@link Director::$environment_type}.
26
 *
27
 * @see Director::handleRequest()
28
 * @see Director::$rules
29
 * @see Director::$environment_type
30
 */
31
class Director implements TemplateGlobalProvider
32
{
33
    use Configurable;
34
    use Injectable;
35
    use HTTPMiddlewareAware;
36
37
    /**
38
     * Specifies this url is relative to the base.
39
     *
40
     * @var string
41
     */
42
    const BASE = 'BASE';
43
44
    /**
45
     * Specifies this url is relative to the site root.
46
     *
47
     * @var string
48
     */
49
    const ROOT = 'ROOT';
50
51
    /**
52
     * specifies this url is relative to the current request.
53
     *
54
     * @var string
55
     */
56
    const REQUEST = 'REQUEST';
57
58
    /**
59
     * @config
60
     * @var array
61
     */
62
    private static $rules = array();
63
64
    /**
65
     * Set current page
66
     *
67
     * @internal
68
     * @var SiteTree
69
     */
70
    private static $current_page;
71
72
    /**
73
     * @config
74
     * @var string
75
     */
76
    private static $alternate_base_folder;
77
78
    /**
79
     * Force the base_url to a specific value.
80
     * If assigned, default_base_url and the value in the $_SERVER
81
     * global is ignored.
82
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
83
     *
84
     * @config
85
     * @var string
86
     */
87
    private static $alternate_base_url;
88
89
    /**
90
     * Base url to populate if cannot be determined otherwise.
91
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
92
     *
93
     * @config
94
     * @var string
95
     */
96
    private static $default_base_url = '`SS_BASE_URL`';
97
98
    /**
99
     * Assigned environment type
100
     *
101
     * @internal
102
     * @var string
103
     */
104
    protected static $environment_type;
105
106
    /**
107
     * Test a URL request, returning a response object. This method is a wrapper around
108
     * Director::handleRequest() to assist with functional testing. It will execute the URL given, and
109
     * return the result as an HTTPResponse object.
110
     *
111
     * @param string $url The URL to visit.
112
     * @param array $postVars The $_POST & $_FILES variables.
113
     * @param array|Session $session The {@link Session} object representing the current session.
114
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
115
     * session.
116
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
117
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
118
     * @param string $body The HTTP body.
119
     * @param array $headers HTTP headers with key-value pairs.
120
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
121
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
122
     *
123
     * @return HTTPResponse
124
     *
125
     * @throws HTTPResponse_Exception
126
     */
127
    public static function test(
128
        $url,
129
        $postVars = [],
130
        $session = array(),
131
        $httpMethod = null,
132
        $body = null,
133
        $headers = array(),
134
        $cookies = array(),
135
        &$request = null
136
    ) {
137
        return static::mockRequest(
138
            function (HTTPRequest $request) {
139
                return Director::singleton()->handleRequest($request);
140
            },
141
            $url,
142
            $postVars,
143
            $session,
144
            $httpMethod,
145
            $body,
146
            $headers,
147
            $cookies,
148
            $request
149
        );
150
    }
151
152
    /**
153
     * Mock a request, passing this to the given callback, before resetting.
154
     *
155
     * @param callable $callback Action to pass the HTTPRequst object
156
     * @param string $url The URL to build
157
     * @param array $postVars The $_POST & $_FILES variables.
158
     * @param array|Session $session The {@link Session} object representing the current session.
159
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
160
     * session.
161
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
162
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
163
     * @param string $body The HTTP body.
164
     * @param array $headers HTTP headers with key-value pairs.
165
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
166
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
167
     * @return mixed Result of callback
168
     */
169
    public static function mockRequest(
170
        $callback,
171
        $url,
172
        $postVars = [],
173
        $session = [],
174
        $httpMethod = null,
175
        $body = null,
176
        $headers = [],
177
        $cookies = [],
178
        &$request = null
179
    ) {
180
        // Build list of cleanup promises
181
        $finally = [];
182
183
        /** @var Kernel $kernel */
184
        $kernel = Injector::inst()->get(Kernel::class);
185
        $kernel->nest();
186
        $finally[] = function () use ($kernel) {
187
            $kernel->activate();
188
        };
189
190
        // backup existing vars, and create new vars
191
        $existingVars = Environment::getVariables();
192
        $finally[] = function () use ($existingVars) {
193
            Environment::setVariables($existingVars);
194
        };
195
        $newVars = $existingVars;
196
197
        // These are needed so that calling Director::test() does not muck with whoever is calling it.
198
        // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
199
        if (class_exists(Versioned::class)) {
200
            $oldReadingMode = Versioned::get_reading_mode();
201
            $finally[] = function () use ($oldReadingMode) {
202
                Versioned::set_reading_mode($oldReadingMode);
203
            };
204
        }
205
206
        // Default httpMethod
207
        $newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod ?: ($postVars ? "POST" : "GET");
208
        $newVars['_POST'] = (array)$postVars;
209
210
        // Setup session
211
        if ($session instanceof Session) {
212
            // Note: If passing $session as object, ensure that changes are written back
213
            // This is important for classes such as FunctionalTest which emulate cross-request persistence
214
            $newVars['_SESSION'] = $session->getAll();
215
            $finally[] = function () use ($session) {
216
                if (isset($_SESSION)) {
217
                    foreach ($_SESSION as $key => $value) {
218
                        $session->set($key, $value);
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 (parse_url($url, PHP_URL_HOST)) {
246
            $bits = parse_url($url);
247
248
            // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
249
            $newVars['_SERVER']['HTTP_HOST'] = isset($bits['port'])
250
                ? $bits['host'].':'.$bits['port']
251
                : $bits['host'];
252
        }
253
254
        // Ensure URL is properly made relative.
255
        // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
256
        $url = self::makeRelative($url);
257
        if (strpos($url, '?') !== false) {
258
            list($url, $getVarsEncoded) = explode('?', $url, 2);
259
            parse_str($getVarsEncoded, $newVars['_GET']);
260
        } else {
261
            $newVars['_GET'] = [];
262
        }
263
        $newVars['_SERVER']['REQUEST_URI'] = Director::baseURL() . $url;
264
        $newVars['_REQUEST'] = array_merge($newVars['_GET'], $newVars['_POST']);
265
266
        // Normalise vars
267
        $newVars = HTTPRequestBuilder::cleanEnvironment($newVars);
268
269
        // Create new request
270
        $request = HTTPRequestBuilder::createFromVariables($newVars, $body);
271
        if ($headers) {
272
            foreach ($headers as $k => $v) {
273
                $request->addHeader($k, $v);
274
            }
275
        }
276
277
        // Apply new vars to environment
278
        Environment::setVariables($newVars);
279
280
        try {
281
            // Normal request handling
282
            return call_user_func($callback, $request);
283
        } finally {
284
            // Restore state in reverse order to assignment
285
            foreach (array_reverse($finally) as $callback) {
286
                call_user_func($callback);
287
            }
288
        }
289
    }
290
291
    /**
292
     * Process the given URL, creating the appropriate controller and executing it.
293
     *
294
     * Request processing is handled as follows:
295
     * - Director::handleRequest($request) checks each of the Director rules and identifies a controller
296
     *   to handle this request.
297
     * - Controller::handleRequest($request) is then called.  This will find a rule to handle the URL,
298
     *   and call the rule handling method.
299
     * - RequestHandler::handleRequest($request) is recursively called whenever a rule handling method
300
     *   returns a RequestHandler object.
301
     *
302
     * In addition to request processing, Director will manage the session, and perform the output of
303
     * the actual response to the browser.
304
     *
305
     * @param HTTPRequest $request
306
     * @return HTTPResponse
307
     * @throws HTTPResponse_Exception
308
     */
309
    public function handleRequest(HTTPRequest $request)
310
    {
311
        Injector::inst()->registerService($request, HTTPRequest::class);
312
313
        $rules = Director::config()->uninherited('rules');
314
315
        // Default handler - mo URL rules matched, so return a 404 error.
316
        $handler = function () {
317
            return new HTTPResponse('No URL rule was matched', 404);
318
        };
319
320
        foreach ($rules as $pattern => $controllerOptions) {
321
            // Match pattern
322
            $arguments = $request->match($pattern, true);
323
            if ($arguments == false) {
324
                continue;
325
            }
326
327
            // Normalise route rule
328
            if (is_string($controllerOptions)) {
329
                if (substr($controllerOptions, 0, 2) == '->') {
330
                    $controllerOptions = array('Redirect' => substr($controllerOptions, 2));
331
                } else {
332
                    $controllerOptions = array('Controller' => $controllerOptions);
333
                }
334
            }
335
            $request->setRouteParams($controllerOptions);
336
337
            // controllerOptions provide some default arguments
338
            $arguments = array_merge($controllerOptions, $arguments);
339
340
            // Pop additional tokens from the tokenizer if necessary
341
            if (isset($controllerOptions['_PopTokeniser'])) {
342
                $request->shift($controllerOptions['_PopTokeniser']);
343
            }
344
345
            // Handler for redirection
346
            if (isset($arguments['Redirect'])) {
347
                $handler = function () use ($arguments) {
348
                    // Redirection
349
                    $response = new HTTPResponse();
350
                    $response->redirect(static::absoluteURL($arguments['Redirect']));
351
                    return $response;
352
                };
353
                break;
354
            }
355
356
            /** @var RequestHandler $controllerObj */
357
            $controllerObj = Injector::inst()->create($arguments['Controller']);
358
359
            // Handler for calling a controller
360
            $handler = function (HTTPRequest $request) use ($controllerObj) {
361
                try {
362
                    return $controllerObj->handleRequest($request);
363
                } catch (HTTPResponse_Exception $responseException) {
364
                    return $responseException->getResponse();
365
                }
366
            };
367
            break;
368
        }
369
370
        // Call the handler with the configured middlewares
371
        $response = $this->callMiddleware($request, $handler);
372
373
        // Note that if a different request was previously registered, this will now be lost
374
        // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
375
        Injector::inst()->unregisterNamedObject(HTTPRequest::class);
376
377
        return $response;
378
    }
379
380
    /**
381
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
382
     * object to return, then this will return the current controller.
383
     *
384
     * @return SiteTree|Controller
385
     */
386
    public static function get_current_page()
387
    {
388
        return self::$current_page ? self::$current_page : Controller::curr();
389
    }
390
391
    /**
392
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
393
     *
394
     * @param SiteTree $page
395
     */
396
    public static function set_current_page($page)
397
    {
398
        self::$current_page = $page;
399
    }
400
401
    /**
402
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
403
     * evaluated relative to the current base_url.
404
     *
405
     * @param string $url URL To transform to absolute.
406
     * @param string $relativeParent Method to use for evaluating relative urls.
407
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
408
     * Defaults to BASE, which is the same behaviour as template url resolution.
409
     * Ignored if the url is absolute or site root.
410
     *
411
     * @return string
412
     */
413
    public static function absoluteURL($url, $relativeParent = self::BASE)
414
    {
415
        if (is_bool($relativeParent)) {
416
            // Deprecate old boolean second parameter
417
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
418
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
419
        }
420
421
        // Check if there is already a protocol given
422
        if (preg_match('/^http(s?):\/\//', $url)) {
423
            return $url;
424
        }
425
426
        // Absolute urls without protocol are added
427
        // E.g. //google.com -> http://google.com
428
        if (strpos($url, '//') === 0) {
429
            return self::protocol() . substr($url, 2);
430
        }
431
432
        // Determine method for mapping the parent to this relative url
433
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
434
            // Root relative urls always should be evaluated relative to the root
435
            $parent = self::protocolAndHost();
436
        } elseif ($relativeParent === self::REQUEST) {
437
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
438
            if (!isset($_SERVER['REQUEST_URI'])) {
439
                return false;
440
            }
441
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
442
        } else {
443
            // Default to respecting site base_url
444
            $parent = self::absoluteBaseURL();
445
        }
446
447
        // Map empty urls to relative slash and join to base
448
        if (empty($url) || $url === '.' || $url === './') {
449
            $url = '/';
450
        }
451
        return Controller::join_links($parent, $url);
452
    }
453
454
    /**
455
     * A helper to determine the current hostname used to access the site.
456
     * The following are used to determine the host (in order)
457
     *  - Director.alternate_base_url (if it contains a domain name)
458
     *  - Trusted proxy headers
459
     *  - HTTP Host header
460
     *  - SS_BASE_URL env var
461
     *  - SERVER_NAME
462
     *  - gethostname()
463
     *
464
     * @param HTTPRequest $request
465
     * @return string
466
     */
467
    public static function host(HTTPRequest $request = null)
0 ignored issues
show
host uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
468
    {
469
        // Check if overridden by alternate_base_url
470
        if ($baseURL = self::config()->get('alternate_base_url')) {
471
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
472
            $host = parse_url($baseURL, PHP_URL_HOST);
473
            if ($host) {
474
                return $host;
475
            }
476
        }
477
478
        $request = static::currentRequest($request);
479
        if ($request && ($host = $request->getHeader('Host'))) {
480
            return $host;
481
        }
482
483
        // Check given header
484
        if (isset($_SERVER['HTTP_HOST'])) {
485
            return $_SERVER['HTTP_HOST'];
486
        }
487
488
        // Check base url
489
        if ($baseURL = self::config()->uninherited('default_base_url')) {
490
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
491
            $host = parse_url($baseURL, PHP_URL_HOST);
492
            if ($host) {
493
                return $host;
494
            }
495
        }
496
497
        // Fail over to server_name (least reliable)
498
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
499
    }
500
501
    /**
502
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
503
     * variable isn't set.
504
     *
505
     * @param HTTPRequest $request
506
     * @return bool|string
507
     */
508
    public static function protocolAndHost(HTTPRequest $request = null)
509
    {
510
        return static::protocol($request) . static::host($request);
511
    }
512
513
    /**
514
     * Return the current protocol that the site is running under.
515
     *
516
     * @param HTTPRequest $request
517
     * @return string
518
     */
519
    public static function protocol(HTTPRequest $request = null)
520
    {
521
        return (self::is_https($request)) ? 'https://' : 'http://';
522
    }
523
524
    /**
525
     * Return whether the site is running as under HTTPS.
526
     *
527
     * @param HTTPRequest $request
528
     * @return bool
529
     */
530
    public static function is_https(HTTPRequest $request = null)
531
    {
532
        // Check override from alternate_base_url
533
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
534
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
535
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
536
            if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
537
                return $protocol === 'https';
538
            }
539
        }
540
541
        // Check the current request
542
        $request = static::currentRequest($request);
543
        if ($request && ($scheme = $request->getScheme())) {
544
            return $scheme === 'https';
545
        }
546
547
        // Check default_base_url
548
        if ($baseURL = self::config()->uninherited('default_base_url')) {
549
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
550
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
551
            if ($protocol) {
552
                return $protocol === 'https';
553
            }
554
        }
555
556
        return false;
557
    }
558
559
    /**
560
     * Return the root-relative url for the baseurl
561
     *
562
     * @return string Root-relative url with trailing slash.
563
     */
564
    public static function baseURL()
565
    {
566
        // Check override base_url
567
        $alternate = self::config()->get('alternate_base_url');
568
        if ($alternate) {
569
            $alternate = Injector::inst()->convertServiceProperty($alternate);
570
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
571
        }
572
573
        // Get env base url
574
        $baseURL = rtrim(BASE_URL, '/') . '/';
575
576
        // Check if BASE_SCRIPT_URL is defined
577
        // e.g. `index.php/`
578
        if (defined('BASE_SCRIPT_URL')) {
579
            return $baseURL . BASE_SCRIPT_URL;
580
        }
581
582
        return $baseURL;
583
    }
584
585
    /**
586
     * Returns the root filesystem folder for the site. It will be automatically calculated unless
587
     * it is overridden with {@link setBaseFolder()}.
588
     *
589
     * @return string
590
     */
591
    public static function baseFolder()
592
    {
593
        $alternate = Director::config()->uninherited('alternate_base_folder');
594
        return ($alternate) ? $alternate : BASE_PATH;
595
    }
596
597
    /**
598
     * Turns an absolute URL or folder into one that's relative to the root of the site. This is useful
599
     * when turning a URL into a filesystem reference, or vice versa.
600
     *
601
     * @param string $url Accepts both a URL or a filesystem path.
602
     *
603
     * @return string
604
     */
605
    public static function makeRelative($url)
606
    {
607
        // Allow for the accidental inclusion whitespace and // in the URL
608
        $url = trim(preg_replace('#([^:])//#', '\\1/', $url));
609
610
        $base1 = self::absoluteBaseURL();
611
        $baseDomain = substr($base1, strlen(self::protocol()));
612
613
        // Only bother comparing the URL to the absolute version if $url looks like a URL.
614
        if (preg_match('/^https?[^:]*:\/\//', $url, $matches)) {
615
            $urlProtocol = $matches[0];
616
            $urlWithoutProtocol = substr($url, strlen($urlProtocol));
617
618
            // If we are already looking at baseURL, return '' (substr will return false)
619
            if ($url == $base1) {
620
                return '';
621
            } elseif (substr($url, 0, strlen($base1)) == $base1) {
622
                return substr($url, strlen($base1));
623
            } elseif (substr($base1, -1) == "/" && $url == substr($base1, 0, -1)) {
624
                // Convert http://www.mydomain.com/mysitedir to ''
625
                return "";
626
            }
627
628
            if (substr($urlWithoutProtocol, 0, strlen($baseDomain)) == $baseDomain) {
629
                return substr($urlWithoutProtocol, strlen($baseDomain));
630
            }
631
        }
632
633
        // test for base folder, e.g. /var/www
634
        $base2 = self::baseFolder();
635
        if (substr($url, 0, strlen($base2)) == $base2) {
636
            return substr($url, strlen($base2));
637
        }
638
639
        // Test for relative base url, e.g. mywebsite/ if the full URL is http://localhost/mywebsite/
640
        $base3 = self::baseURL();
641
        if (substr($url, 0, strlen($base3)) == $base3) {
642
            return substr($url, strlen($base3));
643
        }
644
645
        // Test for relative base url, e.g mywebsite/ if the full url is localhost/myswebsite
646
        if (substr($url, 0, strlen($baseDomain)) == $baseDomain) {
647
            return substr($url, strlen($baseDomain));
648
        }
649
650
        // Nothing matched, fall back to returning the original URL
651
        return $url;
652
    }
653
654
    /**
655
     * Returns true if a given path is absolute. Works under both *nix and windows systems.
656
     *
657
     * @param string $path
658
     *
659
     * @return bool
660
     */
661
    public static function is_absolute($path)
662
    {
663
        if (empty($path)) {
664
            return false;
665
        }
666
        if ($path[0] == '/' || $path[0] == '\\') {
667
            return true;
668
        }
669
        return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1;
670
    }
671
672
    /**
673
     * Determine if the url is root relative (i.e. starts with /, but not with //) SilverStripe
674
     * considers root relative urls as a subset of relative urls.
675
     *
676
     * @param string $url
677
     *
678
     * @return bool
679
     */
680
    public static function is_root_relative_url($url)
681
    {
682
        return strpos($url, '/') === 0 && strpos($url, '//') !== 0;
683
    }
684
685
    /**
686
     * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). URLs beginning with "//"
687
     * are treated as absolute, as browsers take this to mean the same protocol as currently being used.
688
     *
689
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
690
     * and avoid phishing attacks by redirecting to an attackers server.
691
     *
692
     * Note: Can't solely rely on PHP's parse_url() , since it is not intended to work with relative URLs
693
     * or for security purposes. filter_var($url, FILTER_VALIDATE_URL) has similar problems.
694
     *
695
     * @param string $url
696
     *
697
     * @return bool
698
     */
699
    public static function is_absolute_url($url)
700
    {
701
        // Strip off the query and fragment parts of the URL before checking
702
        if (($queryPosition = strpos($url, '?')) !== false) {
703
            $url = substr($url, 0, $queryPosition-1);
704
        }
705
        if (($hashPosition = strpos($url, '#')) !== false) {
706
            $url = substr($url, 0, $hashPosition-1);
707
        }
708
        $colonPosition = strpos($url, ':');
709
        $slashPosition = strpos($url, '/');
710
        return (
711
            // Base check for existence of a host on a compliant URL
712
            parse_url($url, PHP_URL_HOST)
713
            // Check for more than one leading slash without a protocol.
714
            // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers,
715
            // and hence a potential security risk. Single leading slashes are not an issue though.
716
            || preg_match('%^\s*/{2,}%', $url)
717
            || (
718
                // If a colon is found, check if it's part of a valid scheme definition
719
                // (meaning its not preceded by a slash).
720
                $colonPosition !== false
721
                && ($slashPosition === false || $colonPosition < $slashPosition)
722
            )
723
        );
724
    }
725
726
    /**
727
     * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()}.
728
     *
729
     * @param string $url
730
     *
731
     * @return bool
732
     */
733
    public static function is_relative_url($url)
734
    {
735
        return !static::is_absolute_url($url);
736
    }
737
738
    /**
739
     * Checks if the given URL is belonging to this "site" (not an external link). That's the case if
740
     * the URL is relative, as defined by {@link is_relative_url()}, or if the host matches
741
     * {@link protocolAndHost()}.
742
     *
743
     * Useful to check before redirecting based on a URL from user submissions through $_GET or $_POST,
744
     * and avoid phishing attacks by redirecting to an attackers server.
745
     *
746
     * @param string $url
747
     *
748
     * @return bool
749
     */
750
    public static function is_site_url($url)
751
    {
752
        $urlHost = parse_url($url, PHP_URL_HOST);
753
        $actualHost = parse_url(self::protocolAndHost(), PHP_URL_HOST);
754
        if ($urlHost && $actualHost && $urlHost == $actualHost) {
755
            return true;
756
        } else {
757
            return self::is_relative_url($url);
758
        }
759
    }
760
761
    /**
762
     * Given a filesystem reference relative to the site root, return the full file-system path.
763
     *
764
     * @param string $file
765
     *
766
     * @return string
767
     */
768
    public static function getAbsFile($file)
769
    {
770
        return self::is_absolute($file) ? $file : Director::baseFolder() . '/' . $file;
771
    }
772
773
    /**
774
     * Returns true if the given file exists. Filename should be relative to the site root.
775
     *
776
     * @param $file
777
     *
778
     * @return bool
779
     */
780
    public static function fileExists($file)
781
    {
782
        // replace any appended query-strings, e.g. /path/to/foo.php?bar=1 to /path/to/foo.php
783
        $file = preg_replace('/([^\?]*)?.*/', '$1', $file);
784
        return file_exists(Director::getAbsFile($file));
785
    }
786
787
    /**
788
     * Returns the Absolute URL of the site root.
789
     *
790
     * @return string
791
     */
792
    public static function absoluteBaseURL()
793
    {
794
        return self::absoluteURL(
795
            self::baseURL(),
796
            self::ROOT
797
        );
798
    }
799
800
    /**
801
     * Returns the Absolute URL of the site root, embedding the current basic-auth credentials into
802
     * the URL.
803
     *
804
     * @param HTTPRequest|null $request
805
     * @return string
806
     */
807
    public static function absoluteBaseURLWithAuth(HTTPRequest $request = null)
0 ignored issues
show
absoluteBaseURLWithAuth uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
808
    {
809
        $login = "";
810
811
        if (isset($_SERVER['PHP_AUTH_USER'])) {
812
            $login = "$_SERVER[PHP_AUTH_USER]:$_SERVER[PHP_AUTH_PW]@";
813
        }
814
815
        return Director::protocol($request) . $login .  static::host($request) . Director::baseURL();
0 ignored issues
show
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
816
    }
817
818
    /**
819
     * Skip any further processing and immediately respond with a redirect to the passed URL.
820
     *
821
     * @param string $destURL
822
     * @throws HTTPResponse_Exception
823
     */
824
    protected static function force_redirect($destURL)
825
    {
826
        // Redirect to installer
827
        $response = new HTTPResponse();
828
        $response->redirect($destURL, 301);
829
        HTTP::add_cache_headers($response);
830
        throw new HTTPResponse_Exception($response);
831
    }
832
833
    /**
834
     * Force the site to run on SSL.
835
     *
836
     * To use, call from _config.php. For example:
837
     * <code>
838
     * if (Director::isLive()) Director::forceSSL();
839
     * </code>
840
     *
841
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
842
     * patterns for matching relative URLs. For example:
843
     * <code>
844
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
845
     * </code>
846
     *
847
     * If you want certain parts of your site protected under a different domain, you can specify
848
     * the domain as an argument:
849
     * <code>
850
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
851
     * </code>
852
     *
853
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
854
     * to ensure that this won't cause usability problems.
855
     *
856
     * CAUTION: This does not respect the site environment mode. You should check this
857
     * as per the above examples using Director::isLive() or Director::isTest() for example.
858
     *
859
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
860
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
861
     * @return bool true if already on SSL, false if doesn't match patterns (or cannot redirect)
862
     * @throws HTTPResponse_Exception Throws exception with redirect, if successful
863
     */
864
    public static function forceSSL($patterns = null, $secureDomain = null)
865
    {
866
        // Already on SSL
867
        if (static::is_https()) {
868
            return true;
869
        }
870
871
        // Can't redirect without a url
872
        if (!isset($_SERVER['REQUEST_URI'])) {
873
            return false;
874
        }
875
876
        if ($patterns) {
877
            $matched = false;
878
            $relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
879
880
            // protect portions of the site based on the pattern
881
            foreach ($patterns as $pattern) {
882
                if (preg_match($pattern, $relativeURL)) {
883
                    $matched = true;
884
                    break;
885
                }
886
            }
887
            if (!$matched) {
888
                return false;
889
            }
890
        }
891
892
        // if an domain is specified, redirect to that instead of the current domain
893
        if (!$secureDomain) {
894
            $secureDomain = static::host();
895
        }
896
        $url = 'https://' . $secureDomain . $_SERVER['REQUEST_URI'];
897
898
        // Force redirect
899
        self::force_redirect($url);
900
        return true;
901
    }
902
903
    /**
904
     * Force a redirect to a domain starting with "www."
905
     */
906
    public static function forceWWW()
907
    {
908
        if (!Director::isDev() && !Director::isTest() && strpos(static::host(), 'www') !== 0) {
909
            $destURL = str_replace(
910
                Director::protocol(),
911
                Director::protocol() . 'www.',
912
                Director::absoluteURL($_SERVER['REQUEST_URI'])
913
            );
914
915
            self::force_redirect($destURL);
916
        }
917
    }
918
919
    /**
920
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
921
     * jQuery or whether a manually set request-parameter 'ajax' is present.
922
     *
923
     * @param HTTPRequest $request
924
     * @return bool
925
     */
926
    public static function is_ajax(HTTPRequest $request = null)
0 ignored issues
show
is_ajax uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
is_ajax uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
927
    {
928
        $request = self::currentRequest($request);
929
        if ($request) {
930
            return $request->isAjax();
931
        } else {
932
            return (
933
                isset($_REQUEST['ajax']) ||
934
                (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
935
            );
936
        }
937
    }
938
939
    /**
940
     * Returns true if this script is being run from the command line rather than the web server.
941
     *
942
     * @return bool
943
     */
944
    public static function is_cli()
945
    {
946
        return php_sapi_name() === "cli";
947
    }
948
949
    /**
950
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
951
     * {@link Director::isLive()}.
952
     *
953
     * @return bool
954
     */
955
    public static function get_environment_type()
956
    {
957
        /** @var Kernel $kernel */
958
        $kernel = Injector::inst()->get(Kernel::class);
959
        return $kernel->getEnvironment();
960
    }
961
962
    /**
963
     * This function will return true if the site is in a live environment. For information about
964
     * environment types, see {@link Director::set_environment_type()}.
965
     *
966
     * @return bool
967
     */
968
    public static function isLive()
969
    {
970
        return self::get_environment_type() === 'live';
971
    }
972
973
    /**
974
     * This function will return true if the site is in a development environment. For information about
975
     * environment types, see {@link Director::set_environment_type()}.
976
     *
977
     * @return bool
978
     */
979
    public static function isDev()
980
    {
981
        return self::get_environment_type() === 'dev';
982
    }
983
984
    /**
985
     * This function will return true if the site is in a test environment. For information about
986
     * environment types, see {@link Director::set_environment_type()}.
987
     *
988
     * @return bool
989
     */
990
    public static function isTest()
991
    {
992
        return self::get_environment_type() === 'test';
993
    }
994
995
    /**
996
     * Returns an array of strings of the method names of methods on the call that should be exposed
997
     * as global variables in the templates.
998
     *
999
     * @return array
1000
     */
1001
    public static function get_template_global_variables()
1002
    {
1003
        return array(
1004
            'absoluteBaseURL',
1005
            'baseURL',
1006
            'is_ajax',
1007
            'isAjax' => 'is_ajax',
1008
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1009
        );
1010
    }
1011
1012
    /**
1013
     * Helper to validate or check the current request object
1014
     *
1015
     * @param HTTPRequest $request
1016
     * @return HTTPRequest Request object if one is both current and valid
1017
     */
1018
    protected static function currentRequest(HTTPRequest $request = null)
1019
    {
1020
        // Ensure we only use a registered HTTPRequest and don't
1021
        // incidentally construct a singleton
1022
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
1023
            $request = Injector::inst()->get(HTTPRequest::class);
1024
        }
1025
        return $request;
1026
    }
1027
}
1028