Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

RequestHandler::setRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use BadMethodCallException;
6
use Exception;
7
use InvalidArgumentException;
8
use ReflectionClass;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Dev\Debug;
12
use SilverStripe\Security\Permission;
13
use SilverStripe\Security\PermissionFailureException;
14
use SilverStripe\Security\Security;
15
use SilverStripe\View\ViewableData;
16
17
/**
18
 * This class is the base class of any SilverStripe object that can be used to handle HTTP requests.
19
 *
20
 * Any RequestHandler object can be made responsible for handling its own segment of the URL namespace.
21
 * The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
22
 * controller is being used.  It will then call {@link handleRequest()} on that Controller, passing it the parameters
23
 * that it parsed from the URL, and the {@link HTTPRequest} that contains the remainder of the URL to be parsed.
24
 *
25
 * You can use ?debug_request=1 to view information about the different components and rule matches for a specific URL.
26
 *
27
 * In SilverStripe, URL parsing is distributed throughout the object graph.  For example, suppose that we have a
28
 * search form that contains a {@link TreeMultiSelectField} named "Groups".  We want to use ajax to load segments of
29
 * this tree as they are needed rather than downloading the tree right at the beginning.  We could use this URL to get
30
 * the tree segment that appears underneath
31
 *
32
 * Group #36: "admin/crm/SearchForm/field/Groups/treesegment/36"
33
 *  - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
34
 *    Matching Director Rule: "admin/crm" => "ModelAdmin" (defined in mysite/_config.php)
35
 *  - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and
36
 *    pass control to that.
37
 *    Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class)
38
 *  - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass
39
 *    control to that.
40
 *    Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class)
41
 *  - TreeMultiselectField will determine that treesegment/36 is handled by its treesegment() method.  This method
42
 *    will return an HTML fragment that is output to the screen.
43
 *    Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
44
 *
45
 * {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
46
 */
47
class RequestHandler extends ViewableData
48
{
49
50
    /**
51
     * Optional url_segment for this request handler
52
     *
53
     * @config
54
     * @var string|null
55
     */
56
    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...
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(
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
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;
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
119
120
    public function __construct()
121
    {
122
        $this->brokenOnConstruct = false;
123
124
        $this->setRequest(new NullHTTPRequest());
125
126
        parent::__construct();
127
    }
128
129
    /**
130
     * Handles URL requests.
131
     *
132
     *  - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
133
     *  - If the rule matches, the named method will be called.
134
     *  - If there is still more URL to be processed, then handleRequest()
135
     *    is called on the object that that method returns.
136
     *
137
     * Once all of the URL has been processed, the final result is returned.
138
     * However, if the final result is an array, this
139
     * array is interpreted as being additional template data to customise the
140
     * 2nd to last result with, rather than an object
141
     * in its own right.  This is most frequently used when a Controller's
142
     * action will return an array of data with which to
143
     * customise the controller.
144
     *
145
     * @param HTTPRequest $request The object that is reponsible for distributing URL parsing
146
     * @return HTTPResponse|RequestHandler|string|array
147
     */
148
    public function handleRequest(HTTPRequest $request)
149
    {
150
        // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
151
        if ($this->brokenOnConstruct) {
152
            $handlerClass = static::class;
153
            throw new BadMethodCallException(
154
                "parent::__construct() needs to be called on {$handlerClass}::__construct()"
155
            );
156
        }
157
158
        $this->setRequest($request);
159
160
        $match = $this->findAction($request);
161
162
        // If nothing matches, return this object
163
        if (!$match) {
0 ignored issues
show
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...
164
            return $this;
165
        }
166
167
        // Start to find what action to call. Start by using what findAction returned
168
        $action = $match['action'];
169
170
        // We used to put "handleAction" as the action on controllers, but (a) this could only be called when
171
        // you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
172
        if ($action == 'handleAction') {
173
            // TODO Fix LeftAndMain usage
174
            // Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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