Completed
Push — authenticator-refactor ( 7dc887...371abb )
by Simon
06:49
created

RequestHandler   F

Complexity

Total Complexity 101

Size/Duplication

Total Lines 638
Duplicated Lines 3.29 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
dl 21
loc 638
rs 1.4589
c 0
b 0
f 0
wmc 101
lcom 1
cbo 17

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A setDataModel() 0 4 1
F handleRequest() 0 96 24
C findAction() 13 35 8
A addBackURLParam() 8 9 2
B handleAction() 0 22 5
B allowedActions() 0 28 6
C hasAction() 0 42 15
A definingClassForAction() 0 16 3
C checkAccessAction() 0 50 15
A httpError() 0 13 1
A getRequest() 0 4 1
A setRequest() 0 5 1
A Link() 0 16 2
A redirect() 0 6 1
B getBackURL() 0 19 7
A getReturnReferer() 0 8 3
A getReferer() 0 8 2
A redirectBack() 0 15 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like RequestHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RequestHandler, and based on these observations, apply Extract Interface, too.

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
     * @param string $link
301
     * @return string
302
     */
303 View Code Duplication
    protected function addBackURLParam($link)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
304
    {
305
        $backURL = $this->getBackURL();
306
        if ($backURL) {
307
            return Controller::join_links($link, '?BackURL=' . urlencode($backURL));
308
        }
309
310
        return $link;
311
    }
312
313
    /**
314
     * Given a request, and an action name, call that action name on this RequestHandler
315
     *
316
     * Must not raise HTTPResponse_Exceptions - instead it should return
317
     *
318
     * @param $request
319
     * @param $action
320
     * @return HTTPResponse
321
     */
322
    protected function handleAction($request, $action)
323
    {
324
        $classMessage = Director::isLive() ? 'on this handler' : 'on class '.static::class;
325
326
        if (!$this->hasMethod($action)) {
327
            return new HTTPResponse("Action '$action' isn't available $classMessage.", 404);
328
        }
329
330
        $res = $this->extend('beforeCallActionHandler', $request, $action);
331
        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...
332
            return reset($res);
333
        }
334
335
        $actionRes = $this->$action($request);
336
337
        $res = $this->extend('afterCallActionHandler', $request, $action, $actionRes);
338
        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...
339
            return reset($res);
340
        }
341
342
        return $actionRes;
343
    }
344
345
    /**
346
     * Get a array of allowed actions defined on this controller,
347
     * any parent classes or extensions.
348
     *
349
     * Caution: Since 3.1, allowed_actions definitions only apply
350
     * to methods on the controller they're defined on,
351
     * so it is recommended to use the $class argument
352
     * when invoking this method.
353
     *
354
     * @param string $limitToClass
355
     * @return array|null
356
     */
357
    public function allowedActions($limitToClass = null)
358
    {
359
        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...
360
            $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...
361
        } else {
362
            $actions = $this->config()->get('allowed_actions');
363
        }
364
365
        if (is_array($actions)) {
366
            if (array_key_exists('*', $actions)) {
367
                throw new InvalidArgumentException("Invalid allowed_action '*'");
368
            }
369
370
            // convert all keys and values to lowercase to
371
            // allow for easier comparison, unless it is a permission code
372
            $actions = array_change_key_case($actions, CASE_LOWER);
373
374
            foreach ($actions as $key => $value) {
375
                if (is_numeric($key)) {
376
                    $actions[$key] = strtolower($value);
377
                }
378
            }
379
380
            return $actions;
381
        } else {
382
            return null;
383
        }
384
    }
385
386
    /**
387
     * Checks if this request handler has a specific action,
388
     * even if the current user cannot access it.
389
     * Includes class ancestry and extensions in the checks.
390
     *
391
     * @param string $action
392
     * @return bool
393
     */
394
    public function hasAction($action)
395
    {
396
        if ($action == 'index') {
397
            return true;
398
        }
399
400
        // Don't allow access to any non-public methods (inspect instance plus all extensions)
401
        $insts = array_merge(array($this), (array) $this->getExtensionInstances());
402
        foreach ($insts as $inst) {
403
            if (!method_exists($inst, $action)) {
404
                continue;
405
            }
406
            $r = new ReflectionClass(get_class($inst));
407
            $m = $r->getMethod($action);
408
            if (!$m || !$m->isPublic()) {
409
                return false;
410
            }
411
        }
412
413
        $action  = strtolower($action);
414
        $actions = $this->allowedActions();
415
416
        // Check if the action is defined in the allowed actions of any ancestry class
417
        // as either a key or value. Note that if the action is numeric, then keys are not
418
        // searched for actions to prevent actual array keys being recognised as actions.
419
        if (is_array($actions)) {
420
            $isKey   = !is_numeric($action) && array_key_exists($action, $actions);
421
            $isValue = in_array($action, $actions, true);
422
            if ($isKey || $isValue) {
423
                return true;
424
            }
425
        }
426
427
        $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...
428
        if (!is_array($actions) || !$actionsWithoutExtra) {
429
            if ($action != 'doInit' && $action != 'run' && method_exists($this, $action)) {
430
                return true;
431
            }
432
        }
433
434
        return false;
435
    }
436
437
    /**
438
     * Return the class that defines the given action, so that we know where to check allowed_actions.
439
     *
440
     * @param string $actionOrigCasing
441
     * @return string
442
     */
443
    protected function definingClassForAction($actionOrigCasing)
444
    {
445
        $action = strtolower($actionOrigCasing);
446
447
        $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...
448
        $insts = array_merge(array($this), (array) $this->getExtensionInstances());
449
        foreach ($insts as $inst) {
450
            if (!method_exists($inst, $action)) {
451
                continue;
452
            }
453
            $r = new ReflectionClass(get_class($inst));
454
            $m = $r->getMethod($actionOrigCasing);
455
            return $m->getDeclaringClass()->getName();
456
        }
457
        return null;
458
    }
459
460
    /**
461
     * Check that the given action is allowed to be called from a URL.
462
     * It will interrogate {@link self::$allowed_actions} to determine this.
463
     *
464
     * @param string $action
465
     * @return bool
466
     * @throws Exception
467
     */
468
    public function checkAccessAction($action)
469
    {
470
        $actionOrigCasing = $action;
471
        $action = strtolower($action);
472
473
        $isAllowed = false;
474
        $isDefined = false;
475
476
        // Get actions for this specific class (without inheritance)
477
        $definingClass = $this->definingClassForAction($actionOrigCasing);
478
        $allowedActions = $this->allowedActions($definingClass);
479
480
        // check if specific action is set
481
        if (isset($allowedActions[$action])) {
482
            $isDefined = true;
483
            $test = $allowedActions[$action];
484
            if ($test === true || $test === 1 || $test === '1') {
485
                // TRUE should always allow access
486
                $isAllowed = true;
487
            } elseif (substr($test, 0, 2) == '->') {
488
                // Determined by custom method with "->" prefix
489
                list($method, $arguments) = ClassInfo::parse_class_spec(substr($test, 2));
490
                $isAllowed = call_user_func_array(array($this, $method), $arguments);
491
            } else {
492
                // Value is a permission code to check the current member against
493
                $isAllowed = Permission::check($test);
494
            }
495
        } elseif (is_array($allowedActions)
496
            && (($key = array_search($action, $allowedActions, true)) !== false)
497
            && is_numeric($key)
498
        ) {
499
            // Allow numeric array notation (search for array value as action instead of key)
500
            $isDefined = true;
501
            $isAllowed = true;
502
        } elseif (is_array($allowedActions) && !count($allowedActions)) {
503
            // If defined as empty array, deny action
504
            $isAllowed = false;
505
        } elseif ($allowedActions === null) {
506
            // If undefined, allow action based on configuration
507
            $isAllowed = false;
508
        }
509
510
        // If we don't have a match in allowed_actions,
511
        // whitelist the 'index' action as well as undefined actions based on configuration.
512
        if (!$isDefined && ($action == 'index' || empty($action))) {
513
            $isAllowed = true;
514
        }
515
516
        return $isAllowed;
517
    }
518
519
    /**
520
     * Throws a HTTP error response encased in a {@link HTTPResponse_Exception}, which is later caught in
521
     * {@link RequestHandler::handleAction()} and returned to the user.
522
     *
523
     * @param int $errorCode
524
     * @param string $errorMessage Plaintext error message
525
     * @uses HTTPResponse_Exception
526
     * @throws HTTPResponse_Exception
527
     */
528
    public function httpError($errorCode, $errorMessage = null)
529
    {
530
        $request = $this->getRequest();
531
532
        // Call a handler method such as onBeforeHTTPError404
533
        $this->extend("onBeforeHTTPError{$errorCode}", $request);
534
535
        // Call a handler method such as onBeforeHTTPError, passing 404 as the first arg
536
        $this->extend('onBeforeHTTPError', $errorCode, $request);
537
538
        // Throw a new exception
539
        throw new HTTPResponse_Exception($errorMessage, $errorCode);
540
    }
541
542
    /**
543
     * Returns the HTTPRequest object that this controller is using.
544
     * Returns a placeholder {@link NullHTTPRequest} object unless
545
     * {@link handleAction()} or {@link handleRequest()} have been called,
546
     * which adds a reference to an actual {@link HTTPRequest} object.
547
     *
548
     * @return HTTPRequest
549
     */
550
    public function getRequest()
551
    {
552
        return $this->request;
553
    }
554
555
    /**
556
     * Typically the request is set through {@link handleAction()}
557
     * or {@link handleRequest()}, but in some based we want to set it manually.
558
     *
559
     * @param HTTPRequest $request
560
     * @return $this
561
     */
562
    public function setRequest($request)
563
    {
564
        $this->request = $request;
565
        return $this;
566
    }
567
568
    /**
569
     * Returns a link to this controller. Overload with your own Link rules if they exist.
570
     *
571
     * @param string $action Optional action
572
     * @return string
573
     */
574
    public function Link($action = null)
575
    {
576
        // Check configured url_segment
577
        $url = $this->config()->get('url_segment');
578
        if ($url) {
579
            return Controller::join_links($url, $action, '/');
580
        }
581
582
        // no link defined by default
583
        trigger_error(
584
            'Request handler '.static::class. ' does not have a url_segment defined. '.
585
            'Relying on this link may be an application error',
586
            E_USER_WARNING
587
        );
588
        return null;
589
    }
590
591
    /**
592
     * Redirect to the given URL.
593
     *
594
     * @param string $url
595
     * @param int $code
596
     * @return HTTPResponse
597
     */
598
    public function redirect($url, $code = 302)
599
    {
600
        $url = Director::absoluteURL($url);
601
        $response = new HTTPResponse();
602
        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 600 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...
603
    }
604
605
    /**
606
     * Safely get the value of the BackURL param, if provided via querystring / posted var
607
     *
608
     * @return string
609
     */
610
    public function getBackURL()
611
    {
612
        $request = $this->getRequest();
613
        if (!$request) {
614
            return null;
615
        }
616
        $backURL = $request->requestVar('BackURL');
617
        // Fall back to X-Backurl header
618
        if (!$backURL && $request->isAjax() && $request->getHeader('X-Backurl')) {
619
            $backURL = $request->getHeader('X-Backurl');
620
        }
621
        if (!$backURL) {
622
            return null;
623
        }
624
        if (Director::is_site_url($backURL)) {
625
            return $backURL;
626
        }
627
        return null;
628
    }
629
630
    /**
631
     * Returns the referer, if it is safely validated as an internal URL
632
     * and can be redirected to.
633
     *
634
     * @internal called from {@see Form::getValidationErrorResponse}
635
     * @return string|null
636
     */
637
    public function getReturnReferer()
638
    {
639
        $referer = $this->getReferer();
640
        if ($referer && Director::is_site_url($referer)) {
641
            return $referer;
642
        }
643
        return null;
644
    }
645
646
    /**
647
     * Get referer
648
     *
649
     * @return string
650
     */
651
    public function getReferer()
652
    {
653
        $request = $this->getRequest();
654
        if (!$request) {
655
            return null;
656
        }
657
        return $request->getHeader('Referer');
658
    }
659
660
    /**
661
     * Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
662
     * This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
663
     * location.href in IE). If none of the two variables is available, it will redirect to the base
664
     * URL (see {@link Director::baseURL()}).
665
     *
666
     * @uses redirect()
667
     *
668
     * @return HTTPResponse
669
     */
670
    public function redirectBack()
671
    {
672
        // Don't cache the redirect back ever
673
        HTTP::set_cache_age(0);
674
675
        // Prefer to redirect to ?BackURL, but fall back to Referer header
676
        // As a last resort redirect to base url
677
        $url = $this->getBackURL()
678
            ?: $this->getReturnReferer()
679
            ?: Director::baseURL();
680
681
        // Only direct to absolute urls
682
        $url = Director::absoluteURL($url);
683
        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 682 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...
684
    }
685
}
686