RequestHandler   F
last analyzed

Complexity

Total Complexity 99

Size/Duplication

Total Lines 629
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 213
dl 0
loc 629
rs 2
c 0
b 0
f 0
wmc 99

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
F handleRequest() 0 96 24
A httpError() 0 12 1
A redirectBack() 0 11 3
A definingClassForAction() 0 15 3
A getRequest() 0 3 1
C checkAccessAction() 0 49 15
A redirect() 0 5 1
A allowedActions() 0 26 6
A addBackURLParam() 0 8 2
A Link() 0 19 2
A getReturnReferer() 0 7 3
A getReferer() 0 7 2
B findAction() 0 37 8
A setRequest() 0 4 1
B getBackURL() 0 18 7
A handleAction() 0 21 5
C hasAction() 0 41 14

How to fix   Complexity   

Complex Class

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.

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 BadMethodCallException;
6
use Exception;
7
use InvalidArgumentException;
8
use ReflectionClass;
9
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
10
use SilverStripe\Core\ClassInfo;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Dev\Debug;
13
use SilverStripe\Security\Permission;
14
use SilverStripe\Security\PermissionFailureException;
15
use SilverStripe\Security\Security;
16
use SilverStripe\View\ViewableData;
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
    /**
52
     * Optional url_segment for this request handler
53
     *
54
     * @config
55
     * @var string|null
56
     */
57
    private static $url_segment = null;
0 ignored issues
show
introduced by
The private property $url_segment is not used, and could be removed.
Loading history...
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 = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
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
     * [
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;
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
120
121
    public function __construct()
122
    {
123
        $this->brokenOnConstruct = false;
124
125
        $this->setRequest(new NullHTTPRequest());
126
127
        parent::__construct();
128
    }
129
130
    /**
131
     * Handles URL requests.
132
     *
133
     *  - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
134
     *  - If the rule matches, the named method will be called.
135
     *  - If there is still more URL to be processed, then handleRequest()
136
     *    is called on the object that that method returns.
137
     *
138
     * Once all of the URL has been processed, the final result is returned.
139
     * However, if the final result is an array, this
140
     * array is interpreted as being additional template data to customise the
141
     * 2nd to last result with, rather than an object
142
     * in its own right.  This is most frequently used when a Controller's
143
     * action will return an array of data with which to
144
     * customise the controller.
145
     *
146
     * @param HTTPRequest $request The object that is reponsible for distributing URL parsing
147
     * @return HTTPResponse|RequestHandler|string|array
148
     */
149
    public function handleRequest(HTTPRequest $request)
150
    {
151
        // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
152
        if ($this->brokenOnConstruct) {
153
            $handlerClass = static::class;
154
            throw new BadMethodCallException(
155
                "parent::__construct() needs to be called on {$handlerClass}::__construct()"
156
            );
157
        }
158
159
        $this->setRequest($request);
160
161
        $match = $this->findAction($request);
162
163
        // If nothing matches, return this object
164
        if (!$match) {
0 ignored issues
show
introduced by
$match is a non-empty array, thus ! $match is always false.
Loading history...
Bug Best Practice introduced by
The expression $match 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...
165
            return $this;
166
        }
167
168
        // Start to find what action to call. Start by using what findAction returned
169
        $action = $match['action'];
170
171
        // We used to put "handleAction" as the action on controllers, but (a) this could only be called when
172
        // you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
173
        if ($action == 'handleAction') {
174
            // TODO Fix LeftAndMain usage
175
            // Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
176
            $action = '$Action';
177
        }
178
179
        // Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
180
        if ($action[0] == '$') {
181
            $action = str_replace("-", "_", $request->latestParam(substr($action, 1)));
182
        }
183
184
        if (!$action) {
185
            if (isset($_REQUEST['debug_request'])) {
186
                Debug::message("Action not set; using default action method name 'index'");
187
            }
188
            $action = "index";
189
        } elseif (!is_string($action)) {
190
            user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
191
        }
192
193
        $classMessage = Director::isLive() ? 'on this handler' : 'on class ' . static::class;
194
195
        try {
196
            if (!$this->hasAction($action)) {
197
                return $this->httpError(404, "Action '$action' isn't available $classMessage.");
198
            }
199
            if (!$this->checkAccessAction($action) || in_array(strtolower($action), ['run', 'doinit'])) {
200
                return $this->httpError(403, "Action '$action' isn't allowed $classMessage.");
201
            }
202
            $result = $this->handleAction($request, $action);
203
        } catch (HTTPResponse_Exception $e) {
204
            return $e->getResponse();
205
        } catch (PermissionFailureException $e) {
206
            $result = Security::permissionFailure(null, $e->getMessage());
207
        }
208
209
        if ($result instanceof HTTPResponse && $result->isError()) {
210
            if (isset($_REQUEST['debug_request'])) {
211
                Debug::message("Rule resulted in HTTP error; breaking");
212
            }
213
            return $result;
214
        }
215
216
        // If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
217
        // parse. It might have its own handler. However, we only do this if we haven't just parsed an
218
        // empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
219
        // actions which return themselves to avoid infinite loops.
220
        $matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
221
        if ($this !== $result
222
            && !$matchedRuleWasEmpty
223
            && ($result instanceof RequestHandler || $result instanceof HasRequestHandler)
224
        ) {
225
            // Expose delegated request handler
226
            if ($result instanceof HasRequestHandler) {
227
                $result = $result->getRequestHandler();
228
            }
229
            $returnValue = $result->handleRequest($request);
230
231
            // Array results can be used to handle
232
            if (is_array($returnValue)) {
233
                $returnValue = $this->customise($returnValue);
234
            }
235
236
            return $returnValue;
237
238
        // If we return some other data, and all the URL is parsed, then return that
239
        } elseif ($request->allParsed()) {
240
            return $result;
241
242
        // But if we have more content on the URL and we don't know what to do with it, return an error.
243
        } else {
244
            return $this->httpError(404, "I can't handle sub-URLs $classMessage.");
245
        }
246
    }
247
248
    /**
249
     * @param HTTPRequest $request
250
     * @return array
251
     */
252
    protected function findAction($request)
253
    {
254
        $handlerClass = static::class;
255
256
        // We stop after RequestHandler; in other words, at ViewableData
257
        while ($handlerClass && $handlerClass != ViewableData::class) {
258
            $urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
259
260
            if ($urlHandlers) {
261
                foreach ($urlHandlers as $rule => $action) {
262
                    if (isset($_REQUEST['debug_request'])) {
263
                        $class = static::class;
264
                        $remaining = $request->remaining();
265
                        Debug::message("Testing '{$rule}' with '{$remaining}' on {$class}");
266
                    }
267
268
                    if ($request->match($rule, true)) {
269
                        if (isset($_REQUEST['debug_request'])) {
270
                            $class = static::class;
271
                            $latestParams = var_export($request->latestParams(), true);
272
                            Debug::message(
273
                                "Rule '{$rule}' matched to action '{$action}' on {$class}. "
274
                                . "Latest request params: {$latestParams}"
275
                            );
276
                        }
277
278
                        return [
279
                            'rule' => $rule,
280
                            'action' => $action,
281
                        ];
282
                    }
283
                }
284
            }
285
286
            $handlerClass = get_parent_class($handlerClass);
287
        }
288
        return null;
289
    }
290
291
    /**
292
     * @param string $link
293
     * @return string
294
     */
295
    protected function addBackURLParam($link)
296
    {
297
        $backURL = $this->getBackURL();
298
        if ($backURL) {
299
            return Controller::join_links($link, '?BackURL=' . urlencode($backURL));
300
        }
301
302
        return $link;
303
    }
304
305
    /**
306
     * Given a request, and an action name, call that action name on this RequestHandler
307
     *
308
     * Must not raise HTTPResponse_Exceptions - instead it should return
309
     *
310
     * @param $request
311
     * @param $action
312
     * @return HTTPResponse
313
     */
314
    protected function handleAction($request, $action)
315
    {
316
        $classMessage = Director::isLive() ? 'on this handler' : 'on class ' . static::class;
317
318
        if (!$this->hasMethod($action)) {
319
            return new HTTPResponse("Action '$action' isn't available $classMessage.", 404);
320
        }
321
322
        $res = $this->extend('beforeCallActionHandler', $request, $action);
323
        if ($res) {
324
            return reset($res);
325
        }
326
327
        $actionRes = $this->$action($request);
328
329
        $res = $this->extend('afterCallActionHandler', $request, $action, $actionRes);
330
        if ($res) {
331
            return reset($res);
332
        }
333
334
        return $actionRes;
335
    }
336
337
    /**
338
     * Get a array of allowed actions defined on this controller,
339
     * any parent classes or extensions.
340
     *
341
     * Caution: Since 3.1, allowed_actions definitions only apply
342
     * to methods on the controller they're defined on,
343
     * so it is recommended to use the $class argument
344
     * when invoking this method.
345
     *
346
     * @param string $limitToClass
347
     * @return array|null
348
     */
349
    public function allowedActions($limitToClass = null)
350
    {
351
        if ($limitToClass) {
352
            $actions = Config::forClass($limitToClass)->get('allowed_actions', true);
353
        } else {
354
            $actions = $this->config()->get('allowed_actions');
355
        }
356
357
        if (is_array($actions)) {
358
            if (array_key_exists('*', $actions)) {
359
                throw new InvalidArgumentException("Invalid allowed_action '*'");
360
            }
361
362
            // convert all keys and values to lowercase to
363
            // allow for easier comparison, unless it is a permission code
364
            $actions = array_change_key_case($actions, CASE_LOWER);
365
366
            foreach ($actions as $key => $value) {
367
                if (is_numeric($key)) {
368
                    $actions[$key] = strtolower($value);
369
                }
370
            }
371
372
            return $actions;
373
        } else {
374
            return null;
375
        }
376
    }
377
378
    /**
379
     * Checks if this request handler has a specific action,
380
     * even if the current user cannot access it.
381
     * Includes class ancestry and extensions in the checks.
382
     *
383
     * @param string $action
384
     * @return bool
385
     */
386
    public function hasAction($action)
387
    {
388
        if ($action == 'index') {
389
            return true;
390
        }
391
392
        // Don't allow access to any non-public methods (inspect instance plus all extensions)
393
        $insts = array_merge([$this], (array) $this->getExtensionInstances());
394
        foreach ($insts as $inst) {
395
            if (!method_exists($inst, $action)) {
396
                continue;
397
            }
398
            $r = new ReflectionClass(get_class($inst));
399
            $m = $r->getMethod($action);
400
            if (!$m || !$m->isPublic()) {
401
                return false;
402
            }
403
        }
404
405
        $action  = strtolower($action);
406
        $actions = $this->allowedActions();
407
408
        // Check if the action is defined in the allowed actions of any ancestry class
409
        // as either a key or value. Note that if the action is numeric, then keys are not
410
        // searched for actions to prevent actual array keys being recognised as actions.
411
        if (is_array($actions)) {
412
            $isKey   = !is_numeric($action) && array_key_exists($action, $actions);
413
            $isValue = in_array($action, $actions, true);
414
            if ($isKey || $isValue) {
415
                return true;
416
            }
417
        }
418
419
        $actionsWithoutExtra = $this->config()->get('allowed_actions', true);
420
        if (!is_array($actions) || !$actionsWithoutExtra) {
421
            if (!in_array(strtolower($action), ['run', 'doinit']) && method_exists($this, $action)) {
422
                return true;
423
            }
424
        }
425
426
        return false;
427
    }
428
429
    /**
430
     * Return the class that defines the given action, so that we know where to check allowed_actions.
431
     *
432
     * @param string $actionOrigCasing
433
     * @return string
434
     */
435
    protected function definingClassForAction($actionOrigCasing)
436
    {
437
        $action = strtolower($actionOrigCasing);
438
439
        $definingClass = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $definingClass is dead and can be removed.
Loading history...
440
        $insts = array_merge([$this], (array) $this->getExtensionInstances());
441
        foreach ($insts as $inst) {
442
            if (!method_exists($inst, $action)) {
443
                continue;
444
            }
445
            $r = new ReflectionClass(get_class($inst));
446
            $m = $r->getMethod($actionOrigCasing);
447
            return $m->getDeclaringClass()->getName();
448
        }
449
        return null;
450
    }
451
452
    /**
453
     * Check that the given action is allowed to be called from a URL.
454
     * It will interrogate {@link self::$allowed_actions} to determine this.
455
     *
456
     * @param string $action
457
     * @return bool
458
     * @throws Exception
459
     */
460
    public function checkAccessAction($action)
461
    {
462
        $actionOrigCasing = $action;
463
        $action = strtolower($action);
464
465
        $isAllowed = false;
466
        $isDefined = false;
467
468
        // Get actions for this specific class (without inheritance)
469
        $definingClass = $this->definingClassForAction($actionOrigCasing);
470
        $allowedActions = $this->allowedActions($definingClass);
471
472
        // check if specific action is set
473
        if (isset($allowedActions[$action])) {
474
            $isDefined = true;
475
            $test = $allowedActions[$action];
476
            if ($test === true || $test === 1 || $test === '1') {
477
                // TRUE should always allow access
478
                $isAllowed = true;
479
            } elseif (substr($test, 0, 2) == '->') {
480
                // Determined by custom method with "->" prefix
481
                list($method, $arguments) = ClassInfo::parse_class_spec(substr($test, 2));
482
                $isAllowed = call_user_func_array([$this, $method], $arguments);
483
            } else {
484
                // Value is a permission code to check the current member against
485
                $isAllowed = Permission::check($test);
486
            }
487
        } elseif (is_array($allowedActions)
0 ignored issues
show
introduced by
The condition is_array($allowedActions) is always false.
Loading history...
488
            && (($key = array_search($action, $allowedActions, true)) !== false)
489
            && is_numeric($key)
490
        ) {
491
            // Allow numeric array notation (search for array value as action instead of key)
492
            $isDefined = true;
493
            $isAllowed = true;
494
        } elseif (is_array($allowedActions) && !count($allowedActions)) {
0 ignored issues
show
introduced by
The condition is_array($allowedActions) is always false.
Loading history...
495
            // If defined as empty array, deny action
496
            $isAllowed = false;
497
        } elseif ($allowedActions === null) {
0 ignored issues
show
introduced by
The condition $allowedActions === null is always true.
Loading history...
498
            // If undefined, allow action based on configuration
499
            $isAllowed = false;
500
        }
501
502
        // If we don't have a match in allowed_actions,
503
        // whitelist the 'index' action as well as undefined actions based on configuration.
504
        if (!$isDefined && ($action == 'index' || empty($action))) {
505
            $isAllowed = true;
506
        }
507
508
        return $isAllowed;
509
    }
510
511
    /**
512
     * Throws a HTTP error response encased in a {@link HTTPResponse_Exception}, which is later caught in
513
     * {@link RequestHandler::handleAction()} and returned to the user.
514
     *
515
     * @param int $errorCode
516
     * @param string $errorMessage Plaintext error message
517
     * @uses HTTPResponse_Exception
518
     * @throws HTTPResponse_Exception
519
     */
520
    public function httpError($errorCode, $errorMessage = null)
521
    {
522
        $request = $this->getRequest();
523
524
        // Call a handler method such as onBeforeHTTPError404
525
        $this->extend("onBeforeHTTPError{$errorCode}", $request, $errorMessage);
526
527
        // Call a handler method such as onBeforeHTTPError, passing 404 as the first arg
528
        $this->extend('onBeforeHTTPError', $errorCode, $request, $errorMessage);
529
530
        // Throw a new exception
531
        throw new HTTPResponse_Exception($errorMessage, $errorCode);
532
    }
533
534
    /**
535
     * Returns the HTTPRequest object that this controller is using.
536
     * Returns a placeholder {@link NullHTTPRequest} object unless
537
     * {@link handleAction()} or {@link handleRequest()} have been called,
538
     * which adds a reference to an actual {@link HTTPRequest} object.
539
     *
540
     * @return HTTPRequest
541
     */
542
    public function getRequest()
543
    {
544
        return $this->request;
545
    }
546
547
    /**
548
     * Typically the request is set through {@link handleAction()}
549
     * or {@link handleRequest()}, but in some based we want to set it manually.
550
     *
551
     * @param HTTPRequest $request
552
     * @return $this
553
     */
554
    public function setRequest($request)
555
    {
556
        $this->request = $request;
557
        return $this;
558
    }
559
560
    /**
561
     * Returns a link to this controller. Overload with your own Link rules if they exist.
562
     *
563
     * @param string $action Optional action
564
     * @return string
565
     */
566
    public function Link($action = null)
567
    {
568
        // Check configured url_segment
569
        $url = $this->config()->get('url_segment');
570
        if ($url) {
571
            $link = Controller::join_links($url, $action, '/');
572
573
            // Give extensions the chance to modify by reference
574
            $this->extend('updateLink', $link, $action);
575
            return $link;
576
        }
577
578
        // no link defined by default
579
        trigger_error(
580
            'Request handler ' . static::class . ' does not have a url_segment defined. '
581
            . 'Relying on this link may be an application error',
582
            E_USER_WARNING
583
        );
584
        return null;
585
    }
586
587
    /**
588
     * Redirect to the given URL.
589
     *
590
     * @param string $url
591
     * @param int $code
592
     * @return HTTPResponse
593
     */
594
    public function redirect($url, $code = 302)
595
    {
596
        $url = Director::absoluteURL($url);
597
        $response = new HTTPResponse();
598
        return $response->redirect($url, $code);
599
    }
600
601
    /**
602
     * Safely get the value of the BackURL param, if provided via querystring / posted var
603
     *
604
     * @return string
605
     */
606
    public function getBackURL()
607
    {
608
        $request = $this->getRequest();
609
        if (!$request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
610
            return null;
611
        }
612
        $backURL = $request->requestVar('BackURL');
613
        // Fall back to X-Backurl header
614
        if (!$backURL && $request->isAjax() && $request->getHeader('X-Backurl')) {
615
            $backURL = $request->getHeader('X-Backurl');
616
        }
617
        if (!$backURL) {
618
            return null;
619
        }
620
        if (Director::is_site_url($backURL)) {
621
            return $backURL;
622
        }
623
        return null;
624
    }
625
626
    /**
627
     * Returns the referer, if it is safely validated as an internal URL
628
     * and can be redirected to.
629
     *
630
     * @internal called from {@see Form::getValidationErrorResponse}
631
     * @return string|null
632
     */
633
    public function getReturnReferer()
634
    {
635
        $referer = $this->getReferer();
636
        if ($referer && Director::is_site_url($referer)) {
637
            return $referer;
638
        }
639
        return null;
640
    }
641
642
    /**
643
     * Get referer
644
     *
645
     * @return string
646
     */
647
    public function getReferer()
648
    {
649
        $request = $this->getRequest();
650
        if (!$request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
651
            return null;
652
        }
653
        return $request->getHeader('Referer');
654
    }
655
656
    /**
657
     * Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
658
     * This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
659
     * location.href in IE). If none of the two variables is available, it will redirect to the base
660
     * URL (see {@link Director::baseURL()}).
661
     *
662
     * @uses redirect()
663
     *
664
     * @return HTTPResponse
665
     */
666
    public function redirectBack()
667
    {
668
        // Prefer to redirect to ?BackURL, but fall back to Referer header
669
        // As a last resort redirect to base url
670
        $url = $this->getBackURL()
671
            ?: $this->getReturnReferer()
672
            ?: Director::baseURL();
673
674
        // Only direct to absolute urls
675
        $url = Director::absoluteURL($url);
676
        return $this->redirect($url);
677
    }
678
}
679