Completed
Push — authenticator-refactor ( 0a18bb...b9e528 )
by Simon
08:12
created

RequestHandler::findAction()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 35
Code Lines 20

Duplication

Lines 13
Ratio 37.14 %

Importance

Changes 0
Metric Value
cc 8
eloc 20
nc 7
nop 1
dl 13
loc 35
rs 5.3846
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\Dev\Debug;
9
use SilverStripe\ORM\DataModel;
10
use SilverStripe\Security\Security;
11
use SilverStripe\Security\PermissionFailureException;
12
use SilverStripe\Security\Permission;
13
use SilverStripe\View\ViewableData;
14
use ReflectionClass;
15
use Exception;
16
use BadMethodCallException;
17
18
/**
19
 * This class is the base class of any SilverStripe object that can be used to handle HTTP requests.
20
 *
21
 * Any RequestHandler object can be made responsible for handling its own segment of the URL namespace.
22
 * The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
23
 * controller is being used.  It will then call {@link handleRequest()} on that Controller, passing it the parameters
24
 * that it parsed from the URL, and the {@link HTTPRequest} that contains the remainder of the URL to be parsed.
25
 *
26
 * You can use ?debug_request=1 to view information about the different components and rule matches for a specific URL.
27
 *
28
 * In SilverStripe, URL parsing is distributed throughout the object graph.  For example, suppose that we have a
29
 * search form that contains a {@link TreeMultiSelectField} named "Groups".  We want to use ajax to load segments of
30
 * this tree as they are needed rather than downloading the tree right at the beginning.  We could use this URL to get
31
 * the tree segment that appears underneath
32
 *
33
 * Group #36: "admin/crm/SearchForm/field/Groups/treesegment/36"
34
 *  - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
35
 *    Matching Director Rule: "admin/crm" => "ModelAdmin" (defined in mysite/_config.php)
36
 *  - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and
37
 *    pass control to that.
38
 *    Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class)
39
 *  - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass
40
 *    control to that.
41
 *    Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class)
42
 *  - TreeMultiselectField will determine that treesegment/36 is handled by its treesegment() method.  This method
43
 *    will return an HTML fragment that is output to the screen.
44
 *    Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
45
 *
46
 * {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
47
 */
48
class RequestHandler extends ViewableData
49
{
50
    /**
51
     * Optional url_segment for this request handler
52
     *
53
     * @config
54
     * @var string|null
55
     */
56
    private static $url_segment = null;
57
58
    /**
59
     * @var HTTPRequest $request The request object that the controller was called with.
60
     * Set in {@link handleRequest()}. Useful to generate the {}
61
     */
62
    protected $request = null;
63
64
    /**
65
     * The DataModel for this request
66
     */
67
    protected $model = null;
68
69
    /**
70
     * This variable records whether RequestHandler::__construct()
71
     * was called or not. Useful for checking if subclasses have
72
     * called parent::__construct()
73
     *
74
     * @var boolean
75
     */
76
    protected $brokenOnConstruct = true;
77
78
    /**
79
     * The default URL handling rules.  This specifies that the next component of the URL corresponds to a method to
80
     * be called on this RequestHandlingData object.
81
     *
82
     * The keys of this array are parse rules.  See {@link HTTPRequest::match()} for a description of the rules
83
     * available.
84
     *
85
     * The values of the array are the method to be called if the rule matches.  If this value starts with a '$', then
86
     * the named parameter of the parsed URL wil be used to determine the method name.
87
     * @config
88
     */
89
    private static $url_handlers = array(
90
        '$Action' => '$Action',
91
    );
92
93
94
    /**
95
     * Define a list of action handling methods that are allowed to be called directly by URLs.
96
     * The variable should be an array of action names. This sample shows the different values that it can contain:
97
     *
98
     * <code>
99
     * array(
100
     *      // someaction can be accessed by anyone, any time
101
     *      'someaction',
102
     *      // So can otheraction
103
     *      'otheraction' => true,
104
     *      // restrictedaction can only be people with ADMIN privilege
105
     *      'restrictedaction' => 'ADMIN',
106
     *      // complexaction can only be accessed if $this->canComplexAction() returns true
107
     *      'complexaction' '->canComplexAction'
108
     *  );
109
     * </code>
110
     *
111
     * Form getters count as URL actions as well, and should be included in allowed_actions.
112
     * Form actions on the other handed (first argument to {@link FormAction()} should NOT be included,
113
     * these are handled separately through {@link Form->httpSubmission}. You can control access on form actions
114
     * either by conditionally removing {@link FormAction} in the form construction,
115
     * or by defining $allowed_actions in your {@link Form} class.
116
     * @config
117
     */
118
    private static $allowed_actions = null;
119
120
    public function __construct()
121
    {
122
        $this->brokenOnConstruct = false;
123
124
        $this->setRequest(new NullHTTPRequest());
125
126
        // This will prevent bugs if setDataModel() isn't called.
127
        $this->model = DataModel::inst();
128
129
        parent::__construct();
130
    }
131
132
    /**
133
     * Set the DataModel for this request.
134
     *
135
     * @param DataModel $model
136
     */
137
    public function setDataModel($model)
138
    {
139
        $this->model = $model;
140
    }
141
142
    /**
143
     * Handles URL requests.
144
     *
145
     *  - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
146
     *  - If the rule matches, the named method will be called.
147
     *  - If there is still more URL to be processed, then handleRequest()
148
     *    is called on the object that that method returns.
149
     *
150
     * Once all of the URL has been processed, the final result is returned.
151
     * However, if the final result is an array, this
152
     * array is interpreted as being additional template data to customise the
153
     * 2nd to last result with, rather than an object
154
     * in its own right.  This is most frequently used when a Controller's
155
     * action will return an array of data with which to
156
     * customise the controller.
157
     *
158
     * @param HTTPRequest $request The object that is reponsible for distributing URL parsing
159
     * @param DataModel $model
160
     * @return HTTPResponse|RequestHandler|string|array
161
     */
162
    public function handleRequest(HTTPRequest $request, DataModel $model)
163
    {
164
        // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
165
        if ($this->brokenOnConstruct) {
166
            $handlerClass = static::class;
167
            throw new BadMethodCallException(
168
                "parent::__construct() needs to be called on {$handlerClass}::__construct()"
169
            );
170
        }
171
172
        $this->setRequest($request);
173
        $this->setDataModel($model);
174
175
        $match = $this->findAction($request);
176
177
        // If nothing matches, return this object
178
        if (!$match) {
179
            return $this;
180
        }
181
182
        // Start to find what action to call. Start by using what findAction returned
183
        $action = $match['action'];
184
185
        // We used to put "handleAction" as the action on controllers, but (a) this could only be called when
186
        // you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
187
        if ($action == 'handleAction') {
188
            // TODO Fix LeftAndMain usage
189
            // Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
190
            $action = '$Action';
191
        }
192
193
        // Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
194
        if ($action[0] == '$') {
195
            $action = str_replace("-", "_", $request->latestParam(substr($action, 1)));
196
        }
197
198
        if (!$action) {
199
            if (isset($_REQUEST['debug_request'])) {
200
                Debug::message("Action not set; using default action method name 'index'");
201
            }
202
            $action = "index";
203
        } elseif (!is_string($action)) {
204
            user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
205
        }
206
207
        $classMessage = Director::isLive() ? 'on this handler' : 'on class '.static::class;
208
209
        try {
210
            if (!$this->hasAction($action)) {
211
                return $this->httpError(404, "Action '$action' isn't available $classMessage.");
212
            }
213
            if (!$this->checkAccessAction($action) || in_array(strtolower($action), array('run', 'doInit'))) {
214
                return $this->httpError(403, "Action '$action' isn't allowed $classMessage.");
215
            }
216
            $result = $this->handleAction($request, $action);
217
        } catch (HTTPResponse_Exception $e) {
218
            return $e->getResponse();
219
        } catch (PermissionFailureException $e) {
220
            $result = Security::permissionFailure(null, $e->getMessage());
221
        }
222
223
        if ($result instanceof HTTPResponse && $result->isError()) {
224
            if (isset($_REQUEST['debug_request'])) {
225
                Debug::message("Rule resulted in HTTP error; breaking");
226
            }
227
            return $result;
228
        }
229
230
        // If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
231
        // parse. It might have its own handler. However, we only do this if we haven't just parsed an
232
        // empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
233
        // actions which return themselves to avoid infinite loops.
234
        $matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
235
        if ($this !== $result && !$matchedRuleWasEmpty && ($result instanceof RequestHandler || $result instanceof HasRequestHandler)) {
236
            // Expose delegated request handler
237
            if ($result instanceof HasRequestHandler) {
238
                $result = $result->getRequestHandler();
239
            }
240
            $returnValue = $result->handleRequest($request, $model);
241
242
            // Array results can be used to handle
243
            if (is_array($returnValue)) {
244
                $returnValue = $this->customise($returnValue);
245
            }
246
247
            return $returnValue;
248
249
        // If we return some other data, and all the URL is parsed, then return that
250
        } elseif ($request->allParsed()) {
251
            return $result;
252
253
        // But if we have more content on the URL and we don't know what to do with it, return an error.
254
        } else {
255
            return $this->httpError(404, "I can't handle sub-URLs $classMessage.");
256
        }
257
    }
258
259
    /**
260
     * @param HTTPRequest $request
261
     * @return array
262
     */
263
    protected function findAction($request)
264
    {
265
        $handlerClass = static::class;
266
267
        // We stop after RequestHandler; in other words, at ViewableData
268
        while ($handlerClass && $handlerClass != ViewableData::class) {
269
            $urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
270
271
            if ($urlHandlers) {
272
                foreach ($urlHandlers as $rule => $action) {
273 View Code Duplication
                    if (isset($_REQUEST['debug_request'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
274
                        $class = static::class;
275
                        $remaining = $request->remaining();
276
                        Debug::message("Testing '{$rule}' with '{$remaining}' on {$class}");
277
                    }
278
279
                    if ($request->match($rule, true)) {
280 View Code Duplication
                        if (isset($_REQUEST['debug_request'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
281
                            $class = static::class;
282
                            $latestParams = var_export($request->latestParams(), true);
283
                            Debug::message(
284
                                "Rule '{$rule}' matched to action '{$action}' on {$class}. ".
285
                                "Latest request params: {$latestParams}"
286
                            );
287
                        }
288
289
                        return array('rule' => $rule, 'action' => $action);
290
                    }
291
                }
292
            }
293
294
            $handlerClass = get_parent_class($handlerClass);
295
        }
296
        return null;
297
    }
298
299
    /**
300
     * Given a request, and an action name, call that action name on this RequestHandler
301
     *
302
     * Must not raise HTTPResponse_Exceptions - instead it should return
303
     *
304
     * @param $request
305
     * @param $action
306
     * @return HTTPResponse
307
     */
308
    protected function handleAction($request, $action)
309
    {
310
        $classMessage = Director::isLive() ? 'on this handler' : 'on class '.static::class;
311
312
        if (!$this->hasMethod($action)) {
313
            return new HTTPResponse("Action '$action' isn't available $classMessage.", 404);
314
        }
315
316
        $res = $this->extend('beforeCallActionHandler', $request, $action);
317
        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...
318
            return reset($res);
319
        }
320
321
        $actionRes = $this->$action($request);
322
323
        $res = $this->extend('afterCallActionHandler', $request, $action, $actionRes);
324
        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...
325
            return reset($res);
326
        }
327
328
        return $actionRes;
329
    }
330
331
    /**
332
     * Get a array of allowed actions defined on this controller,
333
     * any parent classes or extensions.
334
     *
335
     * Caution: Since 3.1, allowed_actions definitions only apply
336
     * to methods on the controller they're defined on,
337
     * so it is recommended to use the $class argument
338
     * when invoking this method.
339
     *
340
     * @param string $limitToClass
341
     * @return array|null
342
     */
343
    public function allowedActions($limitToClass = null)
344
    {
345
        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...
346
            $actions = Config::forClass($limitToClass)->get('allowed_actions', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
347
        } else {
348
            $actions = $this->config()->get('allowed_actions');
349
        }
350
351
        if (is_array($actions)) {
352
            if (array_key_exists('*', $actions)) {
353
                throw new InvalidArgumentException("Invalid allowed_action '*'");
354
            }
355
356
            // convert all keys and values to lowercase to
357
            // allow for easier comparison, unless it is a permission code
358
            $actions = array_change_key_case($actions, CASE_LOWER);
359
360
            foreach ($actions as $key => $value) {
361
                if (is_numeric($key)) {
362
                    $actions[$key] = strtolower($value);
363
                }
364
            }
365
366
            return $actions;
367
        } else {
368
            return null;
369
        }
370
    }
371
372
    /**
373
     * Checks if this request handler has a specific action,
374
     * even if the current user cannot access it.
375
     * Includes class ancestry and extensions in the checks.
376
     *
377
     * @param string $action
378
     * @return bool
379
     */
380
    public function hasAction($action)
381
    {
382
        if ($action == 'index') {
383
            return true;
384
        }
385
386
        // Don't allow access to any non-public methods (inspect instance plus all extensions)
387
        $insts = array_merge(array($this), (array) $this->getExtensionInstances());
388
        foreach ($insts as $inst) {
389
            if (!method_exists($inst, $action)) {
390
                continue;
391
            }
392
            $r = new ReflectionClass(get_class($inst));
393
            $m = $r->getMethod($action);
394
            if (!$m || !$m->isPublic()) {
395
                return false;
396
            }
397
        }
398
399
        $action  = strtolower($action);
400
        $actions = $this->allowedActions();
401
402
        // Check if the action is defined in the allowed actions of any ancestry class
403
        // as either a key or value. Note that if the action is numeric, then keys are not
404
        // searched for actions to prevent actual array keys being recognised as actions.
405
        if (is_array($actions)) {
406
            $isKey   = !is_numeric($action) && array_key_exists($action, $actions);
407
            $isValue = in_array($action, $actions, true);
408
            if ($isKey || $isValue) {
409
                return true;
410
            }
411
        }
412
413
        $actionsWithoutExtra = $this->config()->get('allowed_actions', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
414
        if (!is_array($actions) || !$actionsWithoutExtra) {
415
            if ($action != 'doInit' && $action != 'run' && method_exists($this, $action)) {
416
                return true;
417
            }
418
        }
419
420
        return false;
421
    }
422
423
    /**
424
     * Return the class that defines the given action, so that we know where to check allowed_actions.
425
     *
426
     * @param string $actionOrigCasing
427
     * @return string
428
     */
429
    protected function definingClassForAction($actionOrigCasing)
430
    {
431
        $action = strtolower($actionOrigCasing);
432
433
        $definingClass = null;
0 ignored issues
show
Unused Code introduced by
$definingClass is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
434
        $insts = array_merge(array($this), (array) $this->getExtensionInstances());
435
        foreach ($insts as $inst) {
436
            if (!method_exists($inst, $action)) {
437
                continue;
438
            }
439
            $r = new ReflectionClass(get_class($inst));
440
            $m = $r->getMethod($actionOrigCasing);
441
            return $m->getDeclaringClass()->getName();
442
        }
443
        return null;
444
    }
445
446
    /**
447
     * Check that the given action is allowed to be called from a URL.
448
     * It will interrogate {@link self::$allowed_actions} to determine this.
449
     *
450
     * @param string $action
451
     * @return bool
452
     * @throws Exception
453
     */
454
    public function checkAccessAction($action)
455
    {
456
        $actionOrigCasing = $action;
457
        $action = strtolower($action);
458
459
        $isAllowed = false;
460
        $isDefined = false;
461
462
        // Get actions for this specific class (without inheritance)
463
        $definingClass = $this->definingClassForAction($actionOrigCasing);
464
        $allowedActions = $this->allowedActions($definingClass);
465
466
        // check if specific action is set
467
        if (isset($allowedActions[$action])) {
468
            $isDefined = true;
469
            $test = $allowedActions[$action];
470
            if ($test === true || $test === 1 || $test === '1') {
471
                // TRUE should always allow access
472
                $isAllowed = true;
473
            } elseif (substr($test, 0, 2) == '->') {
474
                // Determined by custom method with "->" prefix
475
                list($method, $arguments) = ClassInfo::parse_class_spec(substr($test, 2));
476
                $isAllowed = call_user_func_array(array($this, $method), $arguments);
477
            } else {
478
                // Value is a permission code to check the current member against
479
                $isAllowed = Permission::check($test);
480
            }
481
        } elseif (is_array($allowedActions)
482
            && (($key = array_search($action, $allowedActions, true)) !== false)
483
            && is_numeric($key)
484
        ) {
485
            // Allow numeric array notation (search for array value as action instead of key)
486
            $isDefined = true;
487
            $isAllowed = true;
488
        } elseif (is_array($allowedActions) && !count($allowedActions)) {
489
            // If defined as empty array, deny action
490
            $isAllowed = false;
491
        } elseif ($allowedActions === null) {
492
            // If undefined, allow action based on configuration
493
            $isAllowed = false;
494
        }
495
496
        // If we don't have a match in allowed_actions,
497
        // whitelist the 'index' action as well as undefined actions based on configuration.
498
        if (!$isDefined && ($action == 'index' || empty($action))) {
499
            $isAllowed = true;
500
        }
501
502
        return $isAllowed;
503
    }
504
505
    /**
506
     * Throws a HTTP error response encased in a {@link HTTPResponse_Exception}, which is later caught in
507
     * {@link RequestHandler::handleAction()} and returned to the user.
508
     *
509
     * @param int $errorCode
510
     * @param string $errorMessage Plaintext error message
511
     * @uses HTTPResponse_Exception
512
     * @throws HTTPResponse_Exception
513
     */
514
    public function httpError($errorCode, $errorMessage = null)
515
    {
516
        $request = $this->getRequest();
517
518
        // Call a handler method such as onBeforeHTTPError404
519
        $this->extend("onBeforeHTTPError{$errorCode}", $request);
520
521
        // Call a handler method such as onBeforeHTTPError, passing 404 as the first arg
522
        $this->extend('onBeforeHTTPError', $errorCode, $request);
523
524
        // Throw a new exception
525
        throw new HTTPResponse_Exception($errorMessage, $errorCode);
526
    }
527
528
    /**
529
     * Returns the HTTPRequest object that this controller is using.
530
     * Returns a placeholder {@link NullHTTPRequest} object unless
531
     * {@link handleAction()} or {@link handleRequest()} have been called,
532
     * which adds a reference to an actual {@link HTTPRequest} object.
533
     *
534
     * @return HTTPRequest
535
     */
536
    public function getRequest()
537
    {
538
        return $this->request;
539
    }
540
541
    /**
542
     * Typically the request is set through {@link handleAction()}
543
     * or {@link handleRequest()}, but in some based we want to set it manually.
544
     *
545
     * @param HTTPRequest $request
546
     * @return $this
547
     */
548
    public function setRequest($request)
549
    {
550
        $this->request = $request;
551
        return $this;
552
    }
553
554
    /**
555
     * Returns a link to this controller. Overload with your own Link rules if they exist.
556
     *
557
     * @param string $action Optional action
558
     * @return string
559
     */
560
    public function Link($action = null)
561
    {
562
        // Check configured url_segment
563
        $url = $this->config()->get('url_segment');
564
        if ($url) {
565
            return Controller::join_links($url, $action, '/');
566
        }
567
568
        // no link defined by default
569
        trigger_error(
570
            'Request handler '.static::class. ' does not have a url_segment defined. '.
571
            'Relying on this link may be an application error',
572
            E_USER_WARNING
573
        );
574
        return null;
575
    }
576
577
    /**
578
     * Redirect to the given URL.
579
     *
580
     * @param string $url
581
     * @param int $code
582
     * @return HTTPResponse
583
     */
584
    public function redirect($url, $code = 302)
585
    {
586
        $url = Director::absoluteURL($url);
587
        $response = new HTTPResponse();
588
        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 586 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...
589
    }
590
591
    /**
592
     * Safely get the value of the BackURL param, if provided via querystring / posted var
593
     *
594
     * @return string
595
     */
596
    public function getBackURL()
597
    {
598
        $request = $this->getRequest();
599
        if (!$request) {
600
            return null;
601
        }
602
        $backURL = $request->requestVar('BackURL');
603
        // Fall back to X-Backurl header
604
        if (!$backURL && $request->isAjax() && $request->getHeader('X-Backurl')) {
605
            $backURL = $request->getHeader('X-Backurl');
606
        }
607
        if (!$backURL) {
608
            return null;
609
        }
610
        if (Director::is_site_url($backURL)) {
611
            return $backURL;
612
        }
613
        return null;
614
    }
615
616
    /**
617
     * Returns the referer, if it is safely validated as an internal URL
618
     * and can be redirected to.
619
     *
620
     * @internal called from {@see Form::getValidationErrorResponse}
621
     * @return string|null
622
     */
623
    public function getReturnReferer()
624
    {
625
        $referer = $this->getReferer();
626
        if ($referer && Director::is_site_url($referer)) {
627
            return $referer;
628
        }
629
        return null;
630
    }
631
632
    /**
633
     * Get referer
634
     *
635
     * @return string
636
     */
637
    public function getReferer()
638
    {
639
        $request = $this->getRequest();
640
        if (!$request) {
641
            return null;
642
        }
643
        return $request->getHeader('Referer');
644
    }
645
646
    /**
647
     * Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
648
     * This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
649
     * location.href in IE). If none of the two variables is available, it will redirect to the base
650
     * URL (see {@link Director::baseURL()}).
651
     *
652
     * @uses redirect()
653
     *
654
     * @return HTTPResponse
655
     */
656
    public function redirectBack()
657
    {
658
        // Don't cache the redirect back ever
659
        HTTP::set_cache_age(0);
660
661
        // Prefer to redirect to ?BackURL, but fall back to Referer header
662
        // As a last resort redirect to base url
663
        $url = $this->getBackURL()
664
            ?: $this->getReturnReferer()
665
            ?: Director::baseURL();
666
667
        // Only direct to absolute urls
668
        $url = Director::absoluteURL($url);
669
        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 668 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...
670
    }
671
}
672