Completed
Push — 4.2.0 ( f03d5e...24bd0a )
by Daniel
12:22 queued 05:26
created

Director::mockRequest()   D

Complexity

Conditions 17
Paths 128

Size

Total Lines 120
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 58
nc 128
nop 9
dl 0
loc 120
rs 4.9833
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

337
            $arguments = array_merge($controllerOptions, /** @scrutinizer ignore-type */ $arguments);
Loading history...
338
339
            // Pop additional tokens from the tokenizer if necessary
340
            if (isset($controllerOptions['_PopTokeniser'])) {
341
                $request->shift($controllerOptions['_PopTokeniser']);
342
            }
343
344
            // Handler for redirection
345
            if (isset($arguments['Redirect'])) {
346
                $handler = function () use ($arguments) {
347
                    // Redirection
348
                    $response = new HTTPResponse();
349
                    $response->redirect(static::absoluteURL($arguments['Redirect']));
350
                    return $response;
351
                };
352
                break;
353
            }
354
355
            /** @var RequestHandler $controllerObj */
356
            $controllerObj = Injector::inst()->create($arguments['Controller']);
357
358
            // Handler for calling a controller
359
            $handler = function (HTTPRequest $request) use ($controllerObj) {
360
                try {
361
                    return $controllerObj->handleRequest($request);
362
                } catch (HTTPResponse_Exception $responseException) {
363
                    return $responseException->getResponse();
364
                }
365
            };
366
            break;
367
        }
368
369
        // Call the handler with the configured middlewares
370
        $response = $this->callMiddleware($request, $handler);
371
372
        // Note that if a different request was previously registered, this will now be lost
373
        // In these cases it's better to use Kernel::nest() prior to kicking off a nested request
374
        Injector::inst()->unregisterNamedObject(HTTPRequest::class);
375
376
        return $response;
377
    }
378
379
    /**
380
     * Return the {@link SiteTree} object that is currently being viewed. If there is no SiteTree
381
     * object to return, then this will return the current controller.
382
     *
383
     * @return SiteTree|Controller
384
     */
385
    public static function get_current_page()
386
    {
387
        return self::$current_page ? self::$current_page : Controller::curr();
388
    }
389
390
    /**
391
     * Set the currently active {@link SiteTree} object that is being used to respond to the request.
392
     *
393
     * @param SiteTree $page
394
     */
395
    public static function set_current_page($page)
396
    {
397
        self::$current_page = $page;
398
    }
399
400
    /**
401
     * Turns the given URL into an absolute URL. By default non-site root relative urls will be
402
     * evaluated relative to the current base_url.
403
     *
404
     * @param string $url URL To transform to absolute.
405
     * @param string $relativeParent Method to use for evaluating relative urls.
406
     * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page).
407
     * Defaults to BASE, which is the same behaviour as template url resolution.
408
     * Ignored if the url is absolute or site root.
409
     *
410
     * @return string
411
     */
412
    public static function absoluteURL($url, $relativeParent = self::BASE)
413
    {
414
        if (is_bool($relativeParent)) {
0 ignored issues
show
introduced by
The condition is_bool($relativeParent) is always false.
Loading history...
415
            // Deprecate old boolean second parameter
416
            Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url');
417
            $relativeParent = $relativeParent ? self::BASE : self::REQUEST;
418
        }
419
420
        // Check if there is already a protocol given
421
        if (preg_match('/^http(s?):\/\//', $url)) {
422
            return $url;
423
        }
424
425
        // Absolute urls without protocol are added
426
        // E.g. //google.com -> http://google.com
427
        if (strpos($url, '//') === 0) {
428
            return self::protocol() . substr($url, 2);
429
        }
430
431
        // Determine method for mapping the parent to this relative url
432
        if ($relativeParent === self::ROOT || self::is_root_relative_url($url)) {
433
            // Root relative urls always should be evaluated relative to the root
434
            $parent = self::protocolAndHost();
435
        } elseif ($relativeParent === self::REQUEST) {
436
            // Request relative urls rely on the REQUEST_URI param (old default behaviour)
437
            if (!isset($_SERVER['REQUEST_URI'])) {
438
                return false;
439
            }
440
            $parent = dirname($_SERVER['REQUEST_URI'] . 'x');
441
        } else {
442
            // Default to respecting site base_url
443
            $parent = self::absoluteBaseURL();
444
        }
445
446
        // Map empty urls to relative slash and join to base
447
        if (empty($url) || $url === '.' || $url === './') {
448
            $url = '/';
449
        }
450
        return Controller::join_links($parent, $url);
451
    }
452
453
    /**
454
     * Return only host (and optional port) part of a url
455
     *
456
     * @param string $url
457
     * @return string|null Hostname, and optional port, or null if not a valid host
458
     */
459
    protected static function parseHost($url)
460
    {
461
        // Get base hostname
462
        $host = parse_url($url, PHP_URL_HOST);
463
        if (!$host) {
464
            return null;
465
        }
466
467
        // Include port
468
        $port = parse_url($url, PHP_URL_PORT);
469
        if ($port) {
470
            $host .= ':' . $port;
471
        }
472
473
        return $host;
474
    }
475
476
    /**
477
     * Validate user and password in URL, disallowing slashes
478
     *
479
     * @param string $url
480
     * @return bool
481
     */
482
    protected static function validateUserAndPass($url)
483
    {
484
        $parsedURL = parse_url($url);
485
486
        // Validate user (disallow slashes)
487
        if (!empty($parsedURL['user']) && strstr($parsedURL['user'], '\\')) {
488
            return false;
489
        }
490
        if (!empty($parsedURL['pass']) && strstr($parsedURL['pass'], '\\')) {
491
            return false;
492
        }
493
494
        return true;
495
    }
496
497
    /**
498
     * A helper to determine the current hostname used to access the site.
499
     * The following are used to determine the host (in order)
500
     *  - Director.alternate_base_url (if it contains a domain name)
501
     *  - Trusted proxy headers
502
     *  - HTTP Host header
503
     *  - SS_BASE_URL env var
504
     *  - SERVER_NAME
505
     *  - gethostname()
506
     *
507
     * @param HTTPRequest $request
508
     * @return string Host name, including port (if present)
509
     */
510
    public static function host(HTTPRequest $request = null)
511
    {
512
        // Check if overridden by alternate_base_url
513
        if ($baseURL = self::config()->get('alternate_base_url')) {
514
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
515
            $host = static::parseHost($baseURL);
0 ignored issues
show
Bug introduced by
It seems like $baseURL can also be of type array; however, parameter $url of SilverStripe\Control\Director::parseHost() 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

515
            $host = static::parseHost(/** @scrutinizer ignore-type */ $baseURL);
Loading history...
516
            if ($host) {
517
                return $host;
518
            }
519
        }
520
521
        $request = static::currentRequest($request);
522
        if ($request && ($host = $request->getHeader('Host'))) {
523
            return $host;
524
        }
525
526
        // Check given header
527
        if (isset($_SERVER['HTTP_HOST'])) {
528
            return $_SERVER['HTTP_HOST'];
529
        }
530
531
        // Check base url
532
        if ($baseURL = self::config()->uninherited('default_base_url')) {
533
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
534
            $host = static::parseHost($baseURL);
535
            if ($host) {
536
                return $host;
537
            }
538
        }
539
540
        // Fail over to server_name (least reliable)
541
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
542
    }
543
544
    /**
545
     * Return port used for the base URL.
546
     * Note, this will be null if not specified, in which case you should assume the default
547
     * port for the current protocol.
548
     *
549
     * @param HTTPRequest $request
550
     * @return int|null
551
     */
552
    public static function port(HTTPRequest $request = null)
553
    {
554
        $host = static::host($request);
555
        return (int)parse_url($host, PHP_URL_PORT) ?: null;
556
    }
557
558
    /**
559
     * Return host name without port
560
     *
561
     * @param HTTPRequest|null $request
562
     * @return string|null
563
     */
564
    public static function hostName(HTTPRequest $request = null)
565
    {
566
        $host = static::host($request);
567
        return parse_url($host, PHP_URL_HOST) ?: null;
568
    }
569
570
    /**
571
     * Returns the domain part of the URL 'http://www.mysite.com'. Returns FALSE is this environment
572
     * variable isn't set.
573
     *
574
     * @param HTTPRequest $request
575
     * @return bool|string
576
     */
577
    public static function protocolAndHost(HTTPRequest $request = null)
578
    {
579
        return static::protocol($request) . static::host($request);
580
    }
581
582
    /**
583
     * Return the current protocol that the site is running under.
584
     *
585
     * @param HTTPRequest $request
586
     * @return string
587
     */
588
    public static function protocol(HTTPRequest $request = null)
589
    {
590
        return (self::is_https($request)) ? 'https://' : 'http://';
591
    }
592
593
    /**
594
     * Return whether the site is running as under HTTPS.
595
     *
596
     * @param HTTPRequest $request
597
     * @return bool
598
     */
599
    public static function is_https(HTTPRequest $request = null)
600
    {
601
        // Check override from alternate_base_url
602
        if ($baseURL = self::config()->uninherited('alternate_base_url')) {
603
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
604
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
0 ignored issues
show
Bug introduced by
It seems like $baseURL 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

604
            $protocol = parse_url(/** @scrutinizer ignore-type */ $baseURL, PHP_URL_SCHEME);
Loading history...
605
            if ($protocol) {
606
                return $protocol === 'https';
607
            }
608
        }
609
610
        // Check the current request
611
        $request = static::currentRequest($request);
612
        if ($request && ($scheme = $request->getScheme())) {
613
            return $scheme === 'https';
614
        }
615
616
        // Check default_base_url
617
        if ($baseURL = self::config()->uninherited('default_base_url')) {
618
            $baseURL = Injector::inst()->convertServiceProperty($baseURL);
619
            $protocol = parse_url($baseURL, PHP_URL_SCHEME);
620
            if ($protocol) {
621
                return $protocol === 'https';
622
            }
623
        }
624
625
        return false;
626
    }
627
628
    /**
629
     * Return the root-relative url for the baseurl
630
     *
631
     * @return string Root-relative url with trailing slash.
632
     */
633
    public static function baseURL()
634
    {
635
        // Check override base_url
636
        $alternate = self::config()->get('alternate_base_url');
637
        if ($alternate) {
638
            $alternate = Injector::inst()->convertServiceProperty($alternate);
639
            return rtrim(parse_url($alternate, PHP_URL_PATH), '/') . '/';
0 ignored issues
show
Bug introduced by
It seems like $alternate can also be of type array; however, parameter $url of parse_url() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

913
        /** @scrutinizer ignore-call */ 
914
        $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...
914
        if ($user) {
915
            $password = $request->getHeader('PHP_AUTH_PW');
916
            $login = sprintf("%s:%s@", $user, $password) ;
917
        } else {
918
            $login = '';
919
        }
920
921
        return Director::protocol($request) . $login . static::host($request) . Director::baseURL();
0 ignored issues
show
Coding Style introduced by
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...
922
    }
923
924
    /**
925
     * Skip any further processing and immediately respond with a redirect to the passed URL.
926
     *
927
     * @param string $destURL
928
     * @throws HTTPResponse_Exception
929
     */
930
    protected static function force_redirect($destURL)
931
    {
932
        // Redirect to installer
933
        $response = new HTTPResponse();
934
        $response->redirect($destURL, 301);
935
        throw new HTTPResponse_Exception($response);
936
    }
937
938
    /**
939
     * Force the site to run on SSL.
940
     *
941
     * To use, call from _config.php. For example:
942
     * <code>
943
     * if (Director::isLive()) Director::forceSSL();
944
     * </code>
945
     *
946
     * If you don't want your entire site to be on SSL, you can pass an array of PCRE regular expression
947
     * patterns for matching relative URLs. For example:
948
     * <code>
949
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'));
950
     * </code>
951
     *
952
     * If you want certain parts of your site protected under a different domain, you can specify
953
     * the domain as an argument:
954
     * <code>
955
     * if (Director::isLive()) Director::forceSSL(array('/^admin/', '/^Security/'), 'secure.mysite.com');
956
     * </code>
957
     *
958
     * Note that the session data will be lost when moving from HTTP to HTTPS. It is your responsibility
959
     * to ensure that this won't cause usability problems.
960
     *
961
     * CAUTION: This does not respect the site environment mode. You should check this
962
     * as per the above examples using Director::isLive() or Director::isTest() for example.
963
     *
964
     * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
965
     * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
966
     * Can include port number.
967
     * @param HTTPRequest|null $request Request object to check
968
     */
969
    public static function forceSSL($patterns = null, $secureDomain = null, HTTPRequest $request = null)
970
    {
971
        $handler = CanonicalURLMiddleware::singleton()->setForceSSL(true);
972
        if ($patterns) {
973
            $handler->setForceSSLPatterns($patterns);
974
        }
975
        if ($secureDomain) {
976
            $handler->setForceSSLDomain($secureDomain);
977
        }
978
        $handler->throwRedirectIfNeeded($request);
979
    }
980
981
    /**
982
     * Force a redirect to a domain starting with "www."
983
     *
984
     * @param HTTPRequest $request
985
     */
986
    public static function forceWWW(HTTPRequest $request = null)
987
    {
988
        $handler = CanonicalURLMiddleware::singleton()->setForceWWW(true);
989
        $handler->throwRedirectIfNeeded($request);
990
    }
991
992
    /**
993
     * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by
994
     * jQuery or whether a manually set request-parameter 'ajax' is present.
995
     *
996
     * @param HTTPRequest $request
997
     * @return bool
998
     */
999
    public static function is_ajax(HTTPRequest $request = null)
1000
    {
1001
        $request = self::currentRequest($request);
1002
        if ($request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
1003
            return $request->isAjax();
1004
        } else {
1005
            return (
1006
                isset($_REQUEST['ajax']) ||
1007
                (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
1008
            );
1009
        }
1010
    }
1011
1012
    /**
1013
     * Returns true if this script is being run from the command line rather than the web server.
1014
     *
1015
     * @return bool
1016
     */
1017
    public static function is_cli()
1018
    {
1019
        return in_array(php_sapi_name(), ['cli', 'phpdbg']);
1020
    }
1021
1022
    /**
1023
     * Can also be checked with {@link Director::isDev()}, {@link Director::isTest()}, and
1024
     * {@link Director::isLive()}.
1025
     *
1026
     * @return string
1027
     */
1028
    public static function get_environment_type()
1029
    {
1030
        /** @var Kernel $kernel */
1031
        $kernel = Injector::inst()->get(Kernel::class);
1032
        return $kernel->getEnvironment();
1033
    }
1034
1035
    /**
1036
     * This function will return true if the site is in a live environment. For information about
1037
     * environment types, see {@link Director::set_environment_type()}.
1038
     *
1039
     * @return bool
1040
     */
1041
    public static function isLive()
1042
    {
1043
        return self::get_environment_type() === 'live';
1044
    }
1045
1046
    /**
1047
     * This function will return true if the site is in a development environment. For information about
1048
     * environment types, see {@link Director::set_environment_type()}.
1049
     *
1050
     * @return bool
1051
     */
1052
    public static function isDev()
1053
    {
1054
        return self::get_environment_type() === 'dev';
1055
    }
1056
1057
    /**
1058
     * This function will return true if the site is in a test environment. For information about
1059
     * environment types, see {@link Director::set_environment_type()}.
1060
     *
1061
     * @return bool
1062
     */
1063
    public static function isTest()
1064
    {
1065
        return self::get_environment_type() === 'test';
1066
    }
1067
1068
    /**
1069
     * Returns an array of strings of the method names of methods on the call that should be exposed
1070
     * as global variables in the templates.
1071
     *
1072
     * @return array
1073
     */
1074
    public static function get_template_global_variables()
1075
    {
1076
        return array(
1077
            'absoluteBaseURL',
1078
            'baseURL',
1079
            'is_ajax',
1080
            'isAjax' => 'is_ajax',
1081
            'BaseHref' => 'absoluteBaseURL',    //@deprecated 3.0
1082
        );
1083
    }
1084
1085
    /**
1086
     * Helper to validate or check the current request object
1087
     *
1088
     * @param HTTPRequest $request
1089
     * @return HTTPRequest Request object if one is both current and valid
1090
     */
1091
    protected static function currentRequest(HTTPRequest $request = null)
1092
    {
1093
        // Ensure we only use a registered HTTPRequest and don't
1094
        // incidentally construct a singleton
1095
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
1096
            $request = Injector::inst()->get(HTTPRequest::class);
1097
        }
1098
        return $request;
1099
    }
1100
}
1101