Completed
Push — master ( 644ae6...bba86b )
by Daniel
10:38
created

RequestHandler::setRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use InvalidArgumentException;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Object;
9
use SilverStripe\Dev\Debug;
10
use SilverStripe\ORM\DataModel;
11
use SilverStripe\Security\Security;
12
use SilverStripe\Security\PermissionFailureException;
13
use SilverStripe\Security\Permission;
14
use SilverStripe\View\ViewableData;
15
use ReflectionClass;
16
use Exception;
17
use BadMethodCallException;
18
19
/**
20
 * This class is the base class of any SilverStripe object that can be used to handle HTTP requests.
21
 *
22
 * Any RequestHandler object can be made responsible for handling its own segment of the URL namespace.
23
 * The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
24
 * controller is being used.  It will then call {@link handleRequest()} on that Controller, passing it the parameters
25
 * that it parsed from the URL, and the {@link HTTPRequest} that contains the remainder of the URL to be parsed.
26
 *
27
 * You can use ?debug_request=1 to view information about the different components and rule matches for a specific URL.
28
 *
29
 * In SilverStripe, URL parsing is distributed throughout the object graph.  For example, suppose that we have a
30
 * search form that contains a {@link TreeMultiSelectField} named "Groups".  We want to use ajax to load segments of
31
 * this tree as they are needed rather than downloading the tree right at the beginning.  We could use this URL to get
32
 * the tree segment that appears underneath
33
 *
34
 * Group #36: "admin/crm/SearchForm/field/Groups/treesegment/36"
35
 *  - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
36
 *    Matching Director Rule: "admin/crm" => "ModelAdmin" (defined in mysite/_config.php)
37
 *  - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and
38
 *    pass control to that.
39
 *    Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class)
40
 *  - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass
41
 *    control to that.
42
 *    Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class)
43
 *  - TreeMultiselectField will determine that treesegment/36 is handled by its treesegment() method.  This method
44
 *    will return an HTML fragment that is output to the screen.
45
 *    Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
46
 *
47
 * {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
48
 */
49
class RequestHandler extends ViewableData
50
{
51
    /**
52
     * Optional url_segment for this request handler
53
     *
54
     * @config
55
     * @var string|null
56
     */
57
    private static $url_segment = null;
58
59
    /**
60
     * @var HTTPRequest $request The request object that the controller was called with.
61
     * Set in {@link handleRequest()}. Useful to generate the {}
62
     */
63
    protected $request = null;
64
65
    /**
66
     * The DataModel for this request
67
     */
68
    protected $model = null;
69
70
    /**
71
     * This variable records whether RequestHandler::__construct()
72
     * was called or not. Useful for checking if subclasses have
73
     * called parent::__construct()
74
     *
75
     * @var boolean
76
     */
77
    protected $brokenOnConstruct = true;
78
79
    /**
80
     * The default URL handling rules.  This specifies that the next component of the URL corresponds to a method to
81
     * be called on this RequestHandlingData object.
82
     *
83
     * The keys of this array are parse rules.  See {@link HTTPRequest::match()} for a description of the rules
84
     * available.
85
     *
86
     * The values of the array are the method to be called if the rule matches.  If this value starts with a '$', then
87
     * the named parameter of the parsed URL wil be used to determine the method name.
88
     * @config
89
     */
90
    private static $url_handlers = array(
91
        '$Action' => '$Action',
92
    );
93
94
95
    /**
96
     * Define a list of action handling methods that are allowed to be called directly by URLs.
97
     * The variable should be an array of action names. This sample shows the different values that it can contain:
98
     *
99
     * <code>
100
     * array(
101
     *      // someaction can be accessed by anyone, any time
102
     *      'someaction',
103
     *      // So can otheraction
104
     *      'otheraction' => true,
105
     *      // restrictedaction can only be people with ADMIN privilege
106
     *      'restrictedaction' => 'ADMIN',
107
     *      // complexaction can only be accessed if $this->canComplexAction() returns true
108
     *      'complexaction' '->canComplexAction'
109
     *  );
110
     * </code>
111
     *
112
     * Form getters count as URL actions as well, and should be included in allowed_actions.
113
     * Form actions on the other handed (first argument to {@link FormAction()} should NOT be included,
114
     * these are handled separately through {@link Form->httpSubmission}. You can control access on form actions
115
     * either by conditionally removing {@link FormAction} in the form construction,
116
     * or by defining $allowed_actions in your {@link Form} class.
117
     * @config
118
     */
119
    private static $allowed_actions = null;
120
121
    public function __construct()
122
    {
123
        $this->brokenOnConstruct = false;
124
125
        $this->setRequest(new NullHTTPRequest());
126
127
        // This will prevent bugs if setDataModel() isn't called.
128
        $this->model = DataModel::inst();
129
130
        parent::__construct();
131
    }
132
133
    /**
134
     * Set the DataModel for this request.
135
     *
136
     * @param DataModel $model
137
     */
138
    public function setDataModel($model)
139
    {
140
        $this->model = $model;
141
    }
142
143
    /**
144
     * Handles URL requests.
145
     *
146
     *  - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
147
     *  - If the rule matches, the named method will be called.
148
     *  - If there is still more URL to be processed, then handleRequest()
149
     *    is called on the object that that method returns.
150
     *
151
     * Once all of the URL has been processed, the final result is returned.
152
     * However, if the final result is an array, this
153
     * array is interpreted as being additional template data to customise the
154
     * 2nd to last result with, rather than an object
155
     * in its own right.  This is most frequently used when a Controller's
156
     * action will return an array of data with which to
157
     * customise the controller.
158
     *
159
     * @param HTTPRequest $request The object that is reponsible for distributing URL parsing
160
     * @param DataModel $model
161
     * @return HTTPResponse|RequestHandler|string|array
162
     */
163
    public function handleRequest(HTTPRequest $request, DataModel $model)
0 ignored issues
show
Coding Style introduced by
handleRequest uses the super-global variable $_REQUEST which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
164
    {
165
        // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
166
        if ($this->brokenOnConstruct) {
167
            $handlerClass = get_class($this);
168
            throw new BadMethodCallException(
169
                "parent::__construct() needs to be called on {$handlerClass}::__construct()"
170
            );
171
        }
172
173
        $this->setRequest($request);
174
        $this->setDataModel($model);
175
176
        $match = $this->findAction($request);
177
178
        // If nothing matches, return this object
179
        if (!$match) {
180
            return $this;
181
        }
182
183
        // Start to find what action to call. Start by using what findAction returned
184
        $action = $match['action'];
185
186
        // We used to put "handleAction" as the action on controllers, but (a) this could only be called when
187
        // you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
188
        if ($action == 'handleAction') {
189
            // TODO Fix LeftAndMain usage
190
            // Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
191
            $action = '$Action';
192
        }
193
194
        // Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
195
        if ($action[0] == '$') {
196
            $action = str_replace("-", "_", $request->latestParam(substr($action, 1)));
197
        }
198
199
        if (!$action) {
200
            if (isset($_REQUEST['debug_request'])) {
201
                Debug::message("Action not set; using default action method name 'index'");
202
            }
203
            $action = "index";
204
        } elseif (!is_string($action)) {
205
            user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
206
        }
207
208
        $classMessage = Director::isLive() ? 'on this handler' : 'on class '.get_class($this);
209
210
        try {
211
            if (!$this->hasAction($action)) {
212
                return $this->httpError(404, "Action '$action' isn't available $classMessage.");
213
            }
214
            if (!$this->checkAccessAction($action) || in_array(strtolower($action), array('run', 'doInit'))) {
215
                return $this->httpError(403, "Action '$action' isn't allowed $classMessage.");
216
            }
217
            $result = $this->handleAction($request, $action);
218
        } catch (HTTPResponse_Exception $e) {
219
            return $e->getResponse();
220
        } catch (PermissionFailureException $e) {
221
            $result = Security::permissionFailure(null, $e->getMessage());
222
        }
223
224
        if ($result instanceof HTTPResponse && $result->isError()) {
225
            if (isset($_REQUEST['debug_request'])) {
226
                Debug::message("Rule resulted in HTTP error; breaking");
227
            }
228
            return $result;
229
        }
230
231
        // If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
232
        // parse. It might have its own handler. However, we only do this if we haven't just parsed an
233
        // empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
234
        // actions which return themselves to avoid infinite loops.
235
        $matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
236
        if ($this !== $result && !$matchedRuleWasEmpty && ($result instanceof RequestHandler || $result instanceof HasRequestHandler)) {
237
            // Expose delegated request handler
238
            if ($result instanceof HasRequestHandler) {
239
                $result = $result->getRequestHandler();
240
            }
241
            $returnValue = $result->handleRequest($request, $model);
242
243
            // Array results can be used to handle
244
            if (is_array($returnValue)) {
245
                $returnValue = $this->customise($returnValue);
246
            }
247
248
            return $returnValue;
249
250
        // If we return some other data, and all the URL is parsed, then return that
251
        } elseif ($request->allParsed()) {
252
            return $result;
253
254
        // But if we have more content on the URL and we don't know what to do with it, return an error.
255
        } else {
256
            return $this->httpError(404, "I can't handle sub-URLs $classMessage.");
257
        }
258
    }
259
260
    /**
261
     * @param HTTPRequest $request
262
     * @return array
263
     */
264
    protected function findAction($request)
0 ignored issues
show
Coding Style introduced by
findAction uses the super-global variable $_REQUEST which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
265
    {
266
        $handlerClass = ($this->class) ? $this->class : get_class($this);
267
268
        // We stop after RequestHandler; in other words, at ViewableData
269
        while ($handlerClass && $handlerClass != ViewableData::class) {
270
            $urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
271
272
            if ($urlHandlers) {
273
                foreach ($urlHandlers as $rule => $action) {
274
                    if (isset($_REQUEST['debug_request'])) {
275
                        Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
276
                    }
277
278
                    if ($request->match($rule, true)) {
279
                        if (isset($_REQUEST['debug_request'])) {
280
                            Debug::message(
281
                                "Rule '$rule' matched to action '$action' on $this->class. ".
282
                                "Latest request params: " . var_export($request->latestParams(), true)
283
                            );
284
                        }
285
286
                        return array('rule' => $rule, 'action' => $action);
287
                    }
288
                }
289
            }
290
291
            $handlerClass = get_parent_class($handlerClass);
292
        }
293
        return null;
294
    }
295
296
    /**
297
     * Given a request, and an action name, call that action name on this RequestHandler
298
     *
299
     * Must not raise HTTPResponse_Exceptions - instead it should return
300
     *
301
     * @param $request
302
     * @param $action
303
     * @return HTTPResponse
304
     */
305
    protected function handleAction($request, $action)
306
    {
307
        $classMessage = Director::isLive() ? 'on this handler' : 'on class '.get_class($this);
308
309
        if (!$this->hasMethod($action)) {
310
            return new HTTPResponse("Action '$action' isn't available $classMessage.", 404);
311
        }
312
313
        $res = $this->extend('beforeCallActionHandler', $request, $action);
314
        if ($res) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $res 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...
315
            return reset($res);
316
        }
317
318
        $actionRes = $this->$action($request);
319
320
        $res = $this->extend('afterCallActionHandler', $request, $action, $actionRes);
321
        if ($res) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $res 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...
322
            return reset($res);
323
        }
324
325
        return $actionRes;
326
    }
327
328
    /**
329
     * Get a array of allowed actions defined on this controller,
330
     * any parent classes or extensions.
331
     *
332
     * Caution: Since 3.1, allowed_actions definitions only apply
333
     * to methods on the controller they're defined on,
334
     * so it is recommended to use the $class argument
335
     * when invoking this method.
336
     *
337
     * @param string $limitToClass
338
     * @return array|null
339
     */
340
    public function allowedActions($limitToClass = null)
341
    {
342
        if ($limitToClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limitToClass of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
343
            $actions = Config::forClass($limitToClass)->get('allowed_actions', true);
344
        } else {
345
            $actions = $this->config()->get('allowed_actions');
346
        }
347
348
        if (is_array($actions)) {
349
            if (array_key_exists('*', $actions)) {
350
                throw new InvalidArgumentException("Invalid allowed_action '*'");
351
            }
352
353
            // convert all keys and values to lowercase to
354
            // allow for easier comparison, unless it is a permission code
355
            $actions = array_change_key_case($actions, CASE_LOWER);
356
357
            foreach ($actions as $key => $value) {
358
                if (is_numeric($key)) {
359
                    $actions[$key] = strtolower($value);
360
                }
361
            }
362
363
            return $actions;
364
        } else {
365
            return null;
366
        }
367
    }
368
369
    /**
370
     * Checks if this request handler has a specific action,
371
     * even if the current user cannot access it.
372
     * Includes class ancestry and extensions in the checks.
373
     *
374
     * @param string $action
375
     * @return bool
376
     */
377
    public function hasAction($action)
378
    {
379
        if ($action == 'index') {
380
            return true;
381
        }
382
383
        // Don't allow access to any non-public methods (inspect instance plus all extensions)
384
        $insts = array_merge(array($this), (array) $this->getExtensionInstances());
385
        foreach ($insts as $inst) {
386
            if (!method_exists($inst, $action)) {
387
                continue;
388
            }
389
            $r = new ReflectionClass(get_class($inst));
390
            $m = $r->getMethod($action);
391
            if (!$m || !$m->isPublic()) {
392
                return false;
393
            }
394
        }
395
396
        $action  = strtolower($action);
397
        $actions = $this->allowedActions();
398
399
        // Check if the action is defined in the allowed actions of any ancestry class
400
        // as either a key or value. Note that if the action is numeric, then keys are not
401
        // searched for actions to prevent actual array keys being recognised as actions.
402
        if (is_array($actions)) {
403
            $isKey   = !is_numeric($action) && array_key_exists($action, $actions);
404
            $isValue = in_array($action, $actions, true);
405
            if ($isKey || $isValue) {
406
                return true;
407
            }
408
        }
409
410
        $actionsWithoutExtra = $this->config()->get('allowed_actions', true);
411
        if (!is_array($actions) || !$actionsWithoutExtra) {
412
            if ($action != 'doInit' && $action != 'run' && method_exists($this, $action)) {
413
                return true;
414
            }
415
        }
416
417
        return false;
418
    }
419
420
    /**
421
     * Return the class that defines the given action, so that we know where to check allowed_actions.
422
     *
423
     * @param string $actionOrigCasing
424
     * @return string
425
     */
426
    protected function definingClassForAction($actionOrigCasing)
427
    {
428
        $action = strtolower($actionOrigCasing);
429
430
        $definingClass = null;
431
        $insts = array_merge(array($this), (array) $this->getExtensionInstances());
432
        foreach ($insts as $inst) {
433
            if (!method_exists($inst, $action)) {
434
                continue;
435
            }
436
            $r = new ReflectionClass(get_class($inst));
437
            $m = $r->getMethod($actionOrigCasing);
438
            return $m->getDeclaringClass()->getName();
439
        }
440
        return null;
441
    }
442
443
    /**
444
     * Check that the given action is allowed to be called from a URL.
445
     * It will interrogate {@link self::$allowed_actions} to determine this.
446
     *
447
     * @param string $action
448
     * @return bool
449
     * @throws Exception
450
     */
451
    public function checkAccessAction($action)
452
    {
453
        $actionOrigCasing = $action;
454
        $action = strtolower($action);
455
456
        $isAllowed = false;
457
        $isDefined = false;
458
459
        // Get actions for this specific class (without inheritance)
460
        $definingClass = $this->definingClassForAction($actionOrigCasing);
461
        $allowedActions = $this->allowedActions($definingClass);
462
463
        // check if specific action is set
464
        if (isset($allowedActions[$action])) {
465
            $isDefined = true;
466
            $test = $allowedActions[$action];
467
            if ($test === true || $test === 1 || $test === '1') {
468
                // TRUE should always allow access
469
                $isAllowed = true;
470
            } elseif (substr($test, 0, 2) == '->') {
471
                // Determined by custom method with "->" prefix
472
                list($method, $arguments) = Object::parse_class_spec(substr($test, 2));
473
                $isAllowed = call_user_func_array(array($this, $method), $arguments);
474
            } else {
475
                // Value is a permission code to check the current member against
476
                $isAllowed = Permission::check($test);
477
            }
478
        } elseif (is_array($allowedActions)
479
            && (($key = array_search($action, $allowedActions, true)) !== false)
480
            && is_numeric($key)
481
        ) {
482
            // Allow numeric array notation (search for array value as action instead of key)
483
            $isDefined = true;
484
            $isAllowed = true;
485
        } elseif (is_array($allowedActions) && !count($allowedActions)) {
486
            // If defined as empty array, deny action
487
            $isAllowed = false;
488
        } elseif ($allowedActions === null) {
489
            // If undefined, allow action based on configuration
490
            $isAllowed = false;
491
        }
492
493
        // If we don't have a match in allowed_actions,
494
        // whitelist the 'index' action as well as undefined actions based on configuration.
495
        if (!$isDefined && ($action == 'index' || empty($action))) {
496
            $isAllowed = true;
497
        }
498
499
        return $isAllowed;
500
    }
501
502
    /**
503
     * Throws a HTTP error response encased in a {@link HTTPResponse_Exception}, which is later caught in
504
     * {@link RequestHandler::handleAction()} and returned to the user.
505
     *
506
     * @param int $errorCode
507
     * @param string $errorMessage Plaintext error message
508
     * @uses HTTPResponse_Exception
509
     * @throws HTTPResponse_Exception
510
     */
511
    public function httpError($errorCode, $errorMessage = null)
512
    {
513
        $request = $this->getRequest();
514
515
        // Call a handler method such as onBeforeHTTPError404
516
        $this->extend("onBeforeHTTPError{$errorCode}", $request);
517
518
        // Call a handler method such as onBeforeHTTPError, passing 404 as the first arg
519
        $this->extend('onBeforeHTTPError', $errorCode, $request);
520
521
        // Throw a new exception
522
        throw new HTTPResponse_Exception($errorMessage, $errorCode);
523
    }
524
525
    /**
526
     * Returns the HTTPRequest object that this controller is using.
527
     * Returns a placeholder {@link NullHTTPRequest} object unless
528
     * {@link handleAction()} or {@link handleRequest()} have been called,
529
     * which adds a reference to an actual {@link HTTPRequest} object.
530
     *
531
     * @return HTTPRequest
532
     */
533
    public function getRequest()
534
    {
535
        return $this->request;
536
    }
537
538
    /**
539
     * Typically the request is set through {@link handleAction()}
540
     * or {@link handleRequest()}, but in some based we want to set it manually.
541
     *
542
     * @param HTTPRequest $request
543
     * @return $this
544
     */
545
    public function setRequest($request)
546
    {
547
        $this->request = $request;
548
        return $this;
549
    }
550
551
    /**
552
     * Returns a link to this controller. Overload with your own Link rules if they exist.
553
     *
554
     * @param string $action Optional action
555
     * @return string
556
     */
557
    public function Link($action = null)
558
    {
559
        // Check configured url_segment
560
        $url = $this->config()->get('url_segment');
561
        if ($url) {
562
            return Controller::join_links($url, $action, '/');
563
        }
564
565
        // no link defined by default
566
        trigger_error(
567
            'Request handler '.get_class($this). ' does not have a url_segment defined. '.
568
            'Relying on this link may be an application error',
569
            E_USER_WARNING
570
        );
571
        return null;
572
    }
573
574
    /**
575
     * Redirect to the given URL.
576
     *
577
     * @param string $url
578
     * @param int $code
579
     * @return HTTPResponse
580
     */
581
    public function redirect($url, $code = 302)
582
    {
583
        $url = Director::absoluteURL($url);
584
        $response = new HTTPResponse();
585
        return $response->redirect($url, $code);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \SilverStripe\Control\Director::absoluteURL($url) on line 583 can also be of type false; however, SilverStripe\Control\HTTPResponse::redirect() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
586
    }
587
588
    /**
589
     * Safely get the value of the BackURL param, if provided via querystring / posted var
590
     *
591
     * @return string
592
     */
593
    public function getBackURL()
594
    {
595
        $request = $this->getRequest();
596
        if (!$request) {
597
            return null;
598
        }
599
        $backURL = $request->requestVar('BackURL');
600
        // Fall back to X-Backurl header
601
        if (!$backURL && $request->isAjax() && $request->getHeader('X-Backurl')) {
602
            $backURL = $request->getHeader('X-Backurl');
603
        }
604
        if (!$backURL) {
605
            return null;
606
        }
607
        if (Director::is_site_url($backURL)) {
608
            return $backURL;
609
        }
610
        return null;
611
    }
612
613
    /**
614
     * Returns the referer, if it is safely validated as an internal URL
615
     * and can be redirected to.
616
     *
617
     * @internal called from {@see Form::getValidationErrorResponse}
618
     * @return string|null
619
     */
620
    public function getReturnReferer()
621
    {
622
        $referer = $this->getReferer();
623
        if ($referer && Director::is_site_url($referer)) {
624
            return $referer;
625
        }
626
        return null;
627
    }
628
629
    /**
630
     * Get referer
631
     *
632
     * @return string
633
     */
634
    public function getReferer()
635
    {
636
        $request = $this->getRequest();
637
        if (!$request) {
638
            return null;
639
        }
640
        return $request->getHeader('Referer');
641
    }
642
643
    /**
644
     * Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
645
     * This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
646
     * location.href in IE). If none of the two variables is available, it will redirect to the base
647
     * URL (see {@link Director::baseURL()}).
648
     *
649
     * @uses redirect()
650
     *
651
     * @return HTTPResponse
652
     */
653
    public function redirectBack()
654
    {
655
        // Don't cache the redirect back ever
656
        HTTP::set_cache_age(0);
657
658
        // Prefer to redirect to ?BackURL, but fall back to Referer header
659
        // As a last resort redirect to base url
660
        $url = $this->getBackURL()
661
            ?: $this->getReturnReferer()
662
            ?: Director::baseURL();
663
664
        // Only direct to absolute urls
665
        $url = Director::absoluteURL($url);
666
        return $this->redirect($url);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \SilverStripe\Control\Director::absoluteURL($url) on line 665 can also be of type false; however, SilverStripe\Control\RequestHandler::redirect() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
667
    }
668
}
669