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

src/Control/Director.php (18 issues)

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
6
use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
7
use SilverStripe\Control\Middleware\HTTPMiddlewareAware;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Environment;
10
use SilverStripe\Core\Extensible;
11
use SilverStripe\Core\Injector\Injectable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Kernel;
14
use SilverStripe\Core\Path;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\Versioned\Versioned;
17
use SilverStripe\View\Requirements;
18
use SilverStripe\View\Requirements_Backend;
19
use SilverStripe\View\TemplateGlobalProvider;
20
21
/**
22
 * Director is responsible for processing URLs, and providing environment information.
23
 *
24
 * The most important part of director is {@link Director::handleRequest()}, which is passed an HTTPRequest and will
25
 * execute the appropriate controller.
26
 *
27
 * @see Director::handleRequest()
28
 * @see Director::$rules
29
 * @skipUpgrade
30
 */
31
class Director implements TemplateGlobalProvider
32
{
33
    use Configurable;
34
    use Extensible;
35
    use Injectable;
36
    use HTTPMiddlewareAware;
37
38
    /**
39
     * Specifies this url is relative to the base.
40
     *
41
     * @var string
42
     */
43
    const BASE = 'BASE';
44
45
    /**
46
     * Specifies this url is relative to the site root.
47
     *
48
     * @var string
49
     */
50
    const ROOT = 'ROOT';
51
52
    /**
53
     * specifies this url is relative to the current request.
54
     *
55
     * @var string
56
     */
57
    const REQUEST = 'REQUEST';
58
59
    /**
60
     * @config
61
     * @var array
62
     */
63
    private static $rules = array();
0 ignored issues
show
The private property $rules is not used, and could be removed.
Loading history...
64
65
    /**
66
     * Set current page
67
     *
68
     * @internal
69
     * @var SiteTree
70
     */
71
    private static $current_page;
72
73
    /**
74
     * @config
75
     * @var string
76
     */
77
    private static $alternate_base_folder;
0 ignored issues
show
The private property $alternate_base_folder is not used, and could be removed.
Loading history...
78
79
    /**
80
     * Override PUBLIC_DIR. Set to a non-null value to override.
81
     * Setting to an empty string will disable public dir.
82
     *
83
     * @config
84
     * @var bool|null
85
     */
86
    private static $alternate_public_dir = null;
0 ignored issues
show
The private property $alternate_public_dir is not used, and could be removed.
Loading history...
87
88
    /**
89
     * Base url to populate if cannot be determined otherwise.
90
     * Supports back-ticked vars; E.g. '`SS_BASE_URL`'
91
     *
92
     * @config
93
     * @var string
94
     */
95
    private static $default_base_url = '`SS_BASE_URL`';
0 ignored issues
show
The private property $default_base_url is not used, and could be removed.
Loading history...
96
97
    public function __construct()
98
    {
99
    }
100
101
    /**
102
     * Test a URL request, returning a response object. This method is a wrapper around
103
     * Director::handleRequest() to assist with functional testing. It will execute the URL given, and
104
     * return the result as an HTTPResponse object.
105
     *
106
     * @param string $url The URL to visit.
107
     * @param array $postVars The $_POST & $_FILES variables.
108
     * @param array|Session $session The {@link Session} object representing the current session.
109
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
110
     * session.
111
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
112
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
113
     * @param string $body The HTTP body.
114
     * @param array $headers HTTP headers with key-value pairs.
115
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
116
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
117
     *
118
     * @return HTTPResponse
119
     *
120
     * @throws HTTPResponse_Exception
121
     */
122
    public static function test(
123
        $url,
124
        $postVars = [],
125
        $session = array(),
126
        $httpMethod = null,
127
        $body = null,
128
        $headers = array(),
129
        $cookies = array(),
130
        &$request = null
131
    ) {
132
        return static::mockRequest(
133
            function (HTTPRequest $request) {
134
                return Director::singleton()->handleRequest($request);
135
            },
136
            $url,
137
            $postVars,
138
            $session,
139
            $httpMethod,
140
            $body,
141
            $headers,
142
            $cookies,
143
            $request
144
        );
145
    }
146
147
    /**
148
     * Mock a request, passing this to the given callback, before resetting.
149
     *
150
     * @param callable $callback Action to pass the HTTPRequst object
151
     * @param string $url The URL to build
152
     * @param array $postVars The $_POST & $_FILES variables.
153
     * @param array|Session $session The {@link Session} object representing the current session.
154
     * By passing the same object to multiple  calls of Director::test(), you can simulate a persisted
155
     * session.
156
     * @param string $httpMethod The HTTP method, such as GET or POST.  It will default to POST if
157
     * postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
158
     * @param string $body The HTTP body.
159
     * @param array $headers HTTP headers with key-value pairs.
160
     * @param array|Cookie_Backend $cookies to populate $_COOKIE.
161
     * @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
162
     * @return mixed Result of callback
163
     */
164
    public static function mockRequest(
165
        $callback,
166
        $url,
167
        $postVars = [],
168
        $session = [],
169
        $httpMethod = null,
170
        $body = null,
171
        $headers = [],
172
        $cookies = [],
173
        &$request = null
174
    ) {
175
        // Build list of cleanup promises
176
        $finally = [];
177
178
        /** @var Kernel $kernel */
179
        $kernel = Injector::inst()->get(Kernel::class);
180
        $kernel->nest();
181
        $finally[] = function () use ($kernel) {
182
            $kernel->activate();
183
        };
184
185
        // backup existing vars, and create new vars
186
        $existingVars = Environment::getVariables();
187
        $finally[] = function () use ($existingVars) {
188
            Environment::setVariables($existingVars);
189
        };
190
        $newVars = $existingVars;
191
192
        // These are needed so that calling Director::test() does not muck with whoever is calling it.
193
        // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
194
        if (class_exists(Versioned::class)) {
195
            $oldReadingMode = Versioned::get_reading_mode();
196
            $finally[] = function () use ($oldReadingMode) {
197
                Versioned::set_reading_mode($oldReadingMode);
198
            };
199
        }
200
201
        // Default httpMethod
202
        $newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod ?: ($postVars ? "POST" : "GET");
203
        $newVars['_POST'] = (array)$postVars;
204
205
        // Setup session
206
        if ($session instanceof Session) {
207
            // Note: If passing $session as object, ensure that changes are written back
208
            // This is important for classes such as FunctionalTest which emulate cross-request persistence
209
            $newVars['_SESSION'] = $sessionArray = $session->getAll() ?: [];
210
            $finally[] = function () use ($session, $sessionArray) {
211
                if (isset($_SESSION)) {
212
                    // Set new / updated keys
213
                    foreach ($_SESSION as $key => $value) {
214
                        $session->set($key, $value);
215
                    }
216
                    // Unset removed keys
217
                    foreach (array_diff_key($sessionArray, $_SESSION) as $key => $value) {
218
                        $session->clear($key);
219
                    }
220
                }
221
            };
222
        } else {
223
            $newVars['_SESSION'] = $session ?: [];
224
        }
225
226
        // Setup cookies
227
        $cookieJar = $cookies instanceof Cookie_Backend
228
            ? $cookies
229
            : Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: []));
230
        $newVars['_COOKIE'] = $cookieJar->getAll(false);
231
        Cookie::config()->update('report_errors', false);
232
        Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
233
234
        // Backup requirements
235
        $existingRequirementsBackend = Requirements::backend();
236
        Requirements::set_backend(Requirements_Backend::create());
237
        $finally[] = function () use ($existingRequirementsBackend) {
238
            Requirements::set_backend($existingRequirementsBackend);
239
        };
240
241
        // Strip any hash
242
        $url = strtok($url, '#');
243
244
        // Handle absolute URLs
245
        // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
246
        $urlHostPort = static::parseHost($url);
247
        if ($urlHostPort) {
248
            $newVars['_SERVER']['HTTP_HOST'] = $urlHostPort;
249
        }
250
251
        // Ensure URL is properly made relative.
252
        // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
253
        $url = self::makeRelative($url);
254
        if (strpos($url, '?') !== false) {
255
            list($url, $getVarsEncoded) = explode('?', $url, 2);
256
            parse_str($getVarsEncoded, $newVars['_GET']);
257
        } else {
258
            $newVars['_GET'] = [];
259
        }
260
        $newVars['_SERVER']['REQUEST_URI'] = Director::baseURL() . ltrim($url, '/');
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...
261
        $newVars['_REQUEST'] = array_merge($newVars['_GET'], $newVars['_POST']);
262
263
        // Normalise vars
264
        $newVars = HTTPRequestBuilder::cleanEnvironment($newVars);
265
266
        // Create new request
267
        $request = HTTPRequestBuilder::createFromVariables($newVars, $body, ltrim($url, '/'));
268
        if ($headers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $headers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
269
            foreach ($headers as $k => $v) {
270
                $request->addHeader($k, $v);
271
            }
272
        }
273
274
        // Apply new vars to environment
275
        Environment::setVariables($newVars);
276
277
        try {
278
            // Normal request handling
279
            return call_user_func($callback, $request);
280
        } finally {
281
            // Restore state in reverse order to assignment
282
            foreach (array_reverse($finally) as $callback) {
0 ignored issues
show
$callback is overwriting one of the parameters of this function.
Loading history...
283
                call_user_func($callback);
284
            }
285
        }
286
    }
287
288
    /**
289
     * Process the given URL, creating the appropriate controller and executing it.
290
     *
291
     * Request processing is handled as follows:
292
     * - Director::handleRequest($request) checks each of the Director rules and identifies a controller
293
     *   to handle this request.
294
     * - Controller::handleRequest($request) is then called.  This will find a rule to handle the URL,
295
     *   and call the rule handling method.
296
     * - RequestHandler::handleRequest($request) is recursively called whenever a rule handling method
297
     *   returns a RequestHandler object.
298
     *
299
     * In addition to request processing, Director will manage the session, and perform the output of
300
     * the actual response to the browser.
301
     *
302
     * @param HTTPRequest $request
303
     * @return HTTPResponse
304
     * @throws HTTPResponse_Exception
305
     */
306
    public function handleRequest(HTTPRequest $request)
307
    {
308
        Injector::inst()->registerService($request, HTTPRequest::class);
309
310
        $rules = Director::config()->uninherited('rules');
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...
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)) {
0 ignored issues
show
The condition is_bool($relativeParent) is always false.
Loading history...
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;
0 ignored issues
show
The constant SilverStripe\Control\BASE_SCRIPT_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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');
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...
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]
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...
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));
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...
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');
0 ignored issues
show
The method getHeader() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

Loading history...
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();
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...
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) {
0 ignored issues
show
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
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