Completed
Push — master ( 4e2d22...187618 )
by Will
14:40
created

src/Controllers/CommentingController.php (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace SilverStripe\Comments\Controllers;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\Comments\Extensions\CommentsExtension;
7
use SilverStripe\Comments\Model\Comment;
8
use SilverStripe\Control\Cookie;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Email\Email;
12
use SilverStripe\Control\HTTP;
13
use SilverStripe\Control\HTTPRequest;
14
use SilverStripe\Control\RSS\RSSFeed;
15
use SilverStripe\Control\Session;
16
use SilverStripe\Core\Convert;
17
use SilverStripe\Forms\CompositeField;
18
use SilverStripe\Forms\EmailField;
19
use SilverStripe\Forms\FieldList;
20
use SilverStripe\Forms\Form;
21
use SilverStripe\Forms\FormAction;
22
use SilverStripe\Forms\HiddenField;
23
use SilverStripe\Forms\ReadonlyField;
24
use SilverStripe\Forms\RequiredFields;
25
use SilverStripe\Forms\TextareaField;
26
use SilverStripe\Forms\TextField;
27
use SilverStripe\ORM\DataObject;
28
use SilverStripe\ORM\PaginatedList;
29
use SilverStripe\Security\Member;
30
use SilverStripe\Security\Security;
31
32
/**
33
 * @package comments
34
 */
35
class CommentingController extends Controller
36
{
37
    /**
38
     * {@inheritDoc}
39
     */
40
    private static $allowed_actions = array(
41
        'delete',
42
        'spam',
43
        'ham',
44
        'approve',
45
        'rss',
46
        'CommentsForm',
47
        'reply',
48
        'doPostComment',
49
        'doPreviewComment'
50
    );
51
52
    /**
53
     * {@inheritDoc}
54
     */
55
    private static $url_handlers = array(
56
        'reply/$ParentCommentID//$ID/$OtherID' => 'reply',
57
    );
58
59
    /**
60
     * Fields required for this form
61
     *
62
     * @var array
63
     * @config
64
     */
65
    private static $required_fields = array(
66
        'Name',
67
        'Email',
68
        'Comment'
69
    );
70
71
    /**
72
     * Parent class this commenting form is for
73
     *
74
     * @var string
75
     */
76
    private $parentClass = '';
77
78
    /**
79
     * The record this commenting form is for
80
     *
81
     * @var DataObject
82
     */
83
    private $ownerRecord = null;
84
85
    /**
86
     * Parent controller record
87
     *
88
     * @var Controller
89
     */
90
    private $ownerController = null;
91
92
    /**
93
     * Backup url to return to
94
     *
95
     * @var string
96
     */
97
    protected $fallbackReturnURL = null;
98
99
    /**
100
     * Set the parent class name to use
101
     *
102
     * @param string $class
103
     */
104
    public function setParentClass($class)
105
    {
106
        $this->parentClass = $this->encodeClassName($class);
107
    }
108
109
    /**
110
     * Get the parent class name used
111
     *
112
     * @return string
113
     */
114
    public function getParentClass()
115
    {
116
        return $this->decodeClassName($this->parentClass);
117
    }
118
119
    /**
120
     * Encode a fully qualified class name to a URL-safe version
121
     *
122
     * @param string $input
123
     * @return string
124
     */
125
    public function encodeClassName($input)
126
    {
127
        return str_replace('\\', '-', $input);
128
    }
129
130
    /**
131
     * Decode an "encoded" fully qualified class name back to its original
132
     *
133
     * @param string $input
134
     * @return string
135
     */
136
    public function decodeClassName($input)
137
    {
138
        return str_replace('-', '\\', $input);
139
    }
140
141
    /**
142
     * Set the record this controller is working on
143
     *
144
     * @param DataObject $record
145
     */
146
    public function setOwnerRecord($record)
147
    {
148
        $this->ownerRecord = $record;
149
    }
150
151
    /**
152
     * Get the record
153
     *
154
     * @return DataObject
155
     */
156
    public function getOwnerRecord()
157
    {
158
        return $this->ownerRecord;
159
    }
160
161
    /**
162
     * Set the parent controller
163
     *
164
     * @param Controller $controller
165
     */
166
    public function setOwnerController($controller)
167
    {
168
        $this->ownerController = $controller;
169
    }
170
171
    /**
172
     * Get the parent controller
173
     *
174
     * @return Controller
175
     */
176
    public function getOwnerController()
177
    {
178
        return $this->ownerController;
179
    }
180
181
    /**
182
     * Get the commenting option for the current state
183
     *
184
     * @param string $key
185
     * @return mixed Result if the setting is available, or null otherwise
186
     */
187
    public function getOption($key)
188
    {
189
        // If possible use the current record
190
        if ($record = $this->getOwnerRecord()) {
191
            return $record->getCommentsOption($key);
192
        }
193
194
        // Otherwise a singleton of that record
195
        if ($class = $this->getParentClass()) {
196
            return singleton($class)->getCommentsOption($key);
197
        }
198
199
        // Otherwise just use the default options
200
        return singleton(CommentsExtension::class)->getCommentsOption($key);
201
    }
202
203
    /**
204
     * Workaround for generating the link to this controller
205
     *
206
     * @param  string $action
207
     * @param  int    $id
208
     * @param  string $other
209
     * @return string
210
     */
211
    public function Link($action = '', $id = '', $other = '')
212
    {
213
        return Controller::join_links(Director::baseURL(), 'comments', $action, $id, $other);
214
    }
215
216
    /**
217
     * Outputs the RSS feed of comments
218
     *
219
     * @return HTMLText
220
     */
221
    public function rss()
222
    {
223
        return $this->getFeed($this->request)->outputToBrowser();
224
    }
225
226
    /**
227
     * Return an RSSFeed of comments for a given set of comments or all
228
     * comments on the website.
229
     *
230
     * @param HTTPRequest
231
     *
232
     * @return RSSFeed
233
     */
234
    public function getFeed(HTTPRequest $request)
235
    {
236
        $link = $this->Link('rss');
237
        $class = $this->decodeClassName($request->param('ID'));
238
        $id = $request->param('OtherID');
239
240
        // Support old pageid param
241
        if (!$id && !$class && ($id = $request->getVar('pageid'))) {
242
            $class = SiteTree::class;
243
        }
244
245
        $comments = Comment::get()->filter(array(
246
            'Moderated' => 1,
247
            'IsSpam' => 0,
248
        ));
249
250
        // Check if class filter
251
        if ($class) {
252
            if (!is_subclass_of($class, DataObject::class) || !$class::has_extension(CommentsExtension::class)) {
253
                return $this->httpError(404);
254
            }
255
            $this->setParentClass($class);
256
            $comments = $comments->filter('ParentClass', $class);
257
            $link = Controller::join_links($link, $this->encodeClassName($class));
258
259
            // Check if id filter
260
            if ($id) {
261
                $comments = $comments->filter('ParentID', $id);
262
                $link = Controller::join_links($link, $id);
263
                $this->setOwnerRecord(DataObject::get_by_id($class, $id));
264
            }
265
        }
266
267
        $title = _t('CommentingController.RSSTITLE', "Comments RSS Feed");
268
        $comments = new PaginatedList($comments, $request);
269
        $comments->setPageLength($this->getOption('comments_per_page'));
270
271
        return new RSSFeed(
272
            $comments,
273
            $link,
274
            $title,
275
            $link,
276
            'Title',
277
            'EscapedComment',
278
            'AuthorName'
279
        );
280
    }
281
282
    /**
283
     * Deletes a given {@link Comment} via the URL.
284
     */
285 View Code Duplication
    public function delete()
0 ignored issues
show
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...
286
    {
287
        $comment = $this->getComment();
288
        if (!$comment) {
289
            return $this->httpError(404);
290
        }
291
        if (!$comment->canDelete()) {
292
            return Security::permissionFailure($this, 'You do not have permission to delete this comment');
293
        }
294
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
295
            return $this->httpError(400);
296
        }
297
298
        $comment->delete();
299
300
        return $this->request->isAjax()
301
            ? true
302
            : $this->redirectBack();
303
    }
304
305
    /**
306
     * Marks a given {@link Comment} as spam. Removes the comment from display
307
     */
308 View Code Duplication
    public function spam()
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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...
309
    {
310
        $comment = $this->getComment();
311
        if (!$comment) {
312
            return $this->httpError(404);
313
        }
314
        if (!$comment->canEdit()) {
315
            return Security::permissionFailure($this, 'You do not have permission to edit this comment');
316
        }
317
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
318
            return $this->httpError(400);
319
        }
320
321
        $comment->markSpam();
322
        return $this->renderChangedCommentState($comment);
323
    }
324
325
    /**
326
     * Marks a given {@link Comment} as ham (not spam).
327
     */
328 View Code Duplication
    public function ham()
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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...
329
    {
330
        $comment = $this->getComment();
331
        if (!$comment) {
332
            return $this->httpError(404);
333
        }
334
        if (!$comment->canEdit()) {
335
            return Security::permissionFailure($this, 'You do not have permission to edit this comment');
336
        }
337
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
338
            return $this->httpError(400);
339
        }
340
341
        $comment->markApproved();
342
        return $this->renderChangedCommentState($comment);
343
    }
344
345
    /**
346
     * Marks a given {@link Comment} as approved.
347
     */
348 View Code Duplication
    public function approve()
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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...
349
    {
350
        $comment = $this->getComment();
351
        if (!$comment) {
352
            return $this->httpError(404);
353
        }
354
        if (!$comment->canEdit()) {
355
            return Security::permissionFailure($this, 'You do not have permission to approve this comment');
356
        }
357
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
358
            return $this->httpError(400);
359
        }
360
        $comment->markApproved();
361
        return $this->renderChangedCommentState($comment);
362
    }
363
364
    /**
365
     * Redirect back to referer if available, ensuring that only site URLs
366
     * are allowed to avoid phishing.  If it's an AJAX request render the
367
     * comment in it's new state
368
     */
369
    private function renderChangedCommentState($comment)
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
370
    {
371
        $referer = $this->request->getHeader('Referer');
372
373
        // Render comment using AJAX
374
        if ($this->request->isAjax()) {
375
            return $comment->renderWith('Includes/CommentsInterface_singlecomment');
376
        } else {
377
            // Redirect to either the comment or start of the page
378
            if (empty($referer)) {
379
                return $this->redirectBack();
380
            } else {
381
                // Redirect to the comment, but check for phishing
382
                $url = $referer . '#comment-' . $comment->ID;
383
                // absolute redirection URLs not located on this site may cause phishing
384
                if (Director::is_site_url($url)) {
385
                    return $this->redirect($url);
386
                } else {
387
                    return false;
388
                }
389
            }
390
        }
391
    }
392
393
    /**
394
     * Returns the comment referenced in the URL (by ID). Permission checking
395
     * should be done in the callee.
396
     *
397
     * @return Comment|false
398
     */
399
    public function getComment()
400
    {
401
        $id = isset($this->urlParams['ID']) ? $this->urlParams['ID'] : false;
402
        if ($id) {
403
            $comment = Comment::get()->byId($id);
404
            if ($comment) {
405
                $this->fallbackReturnURL = $comment->Link();
406
                return $comment;
407
            }
408
        }
409
410
        return false;
411
    }
412
413
    /**
414
     * Create a reply form for a specified comment
415
     *
416
     * @param  Comment $comment
417
     * @return Form
418
     */
419
    public function ReplyForm($comment)
420
    {
421
        // Enables multiple forms with different names to use the same handler
422
        $form = $this->CommentsForm();
423
        $form->setName('ReplyForm_' . $comment->ID);
424
        $form->addExtraClass('reply-form');
425
426
        // Load parent into reply form
427
        $form->loadDataFrom(array(
428
            'ParentCommentID' => $comment->ID
429
        ));
430
431
        // Customise action
432
        $form->setFormAction($this->Link('reply', $comment->ID));
433
434
        $this->extend('updateReplyForm', $form);
435
        return $form;
436
    }
437
438
439
    /**
440
     * Request handler for reply form.
441
     * This method will disambiguate multiple reply forms in the same method
442
     *
443
     * @param  HTTPRequest $request
444
     * @throws HTTPResponse_Exception
445
     */
446
    public function reply(HTTPRequest $request)
447
    {
448
        // Extract parent comment from reply and build this way
449
        if ($parentID = $request->param('ParentCommentID')) {
450
            $comment = DataObject::get_by_id(Comment::class, $parentID, true);
451
            if ($comment) {
452
                return $this->ReplyForm($comment);
453
            }
454
        }
455
        return $this->httpError(404);
456
    }
457
458
    /**
459
     * Post a comment form
460
     *
461
     * @return Form
462
     */
463
    public function CommentsForm()
464
    {
465
        $usePreview = $this->getOption('use_preview');
466
467
        $nameRequired = _t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name');
468
        $emailRequired = _t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address');
469
        $emailInvalid = _t('CommentInterface.EMAILADDRESS_MESSAGE_EMAIL', 'Please enter a valid email address');
470
        $urlInvalid = _t('CommentInterface.COMMENT_MESSAGE_URL', 'Please enter a valid URL');
471
        $commentRequired = _t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment');
472
473
        $fields = new FieldList(
474
            $dataFields = new CompositeField(
475
                // Name
476
                $a = TextField::create('Name', _t('CommentInterface.YOURNAME', 'Your name'))
477
                    ->setCustomValidationMessage($nameRequired)
478
                    ->setAttribute('data-msg-required', $nameRequired),
479
                // Email
480
                EmailField::create(
481
                    'Email',
482
                    _t('CommentingController.EMAILADDRESS', 'Your email address (will not be published)')
483
                )
484
                    ->setCustomValidationMessage($emailRequired)
485
                    ->setAttribute('data-msg-required', $emailRequired)
486
                    ->setAttribute('data-msg-email', $emailInvalid)
487
                    ->setAttribute('data-rule-email', true),
488
                // Url
489
                TextField::create('URL', _t('CommentingController.WEBSITEURL', 'Your website URL'))
490
                    ->setAttribute('data-msg-url', $urlInvalid)
491
                    ->setAttribute('data-rule-url', true),
492
                // Comment
493
                TextareaField::create('Comment', _t('CommentingController.COMMENTS', 'Comments'))
494
                    ->setCustomValidationMessage($commentRequired)
495
                    ->setAttribute('data-msg-required', $commentRequired)
496
            ),
497
            HiddenField::create('ParentID'),
498
            HiddenField::create('ParentClassName'),
499
            HiddenField::create('ReturnURL'),
500
            HiddenField::create('ParentCommentID')
501
        );
502
503
        // Preview formatted comment. Makes most sense when shortcodes or
504
        // limited HTML is allowed. Populated by JS/Ajax.
505
        if ($usePreview) {
506
            $fields->insertAfter(
507
                ReadonlyField::create('PreviewComment', _t('CommentInterface.PREVIEWLABEL', 'Preview'))
508
                    ->setAttribute('style', 'display: none'), // enable through JS
509
                'Comment'
510
            );
511
        }
512
513
        $dataFields->addExtraClass('data-fields');
514
515
        // save actions
516
        $actions = new FieldList(
517
            $postAction = new FormAction('doPostComment', _t('CommentInterface.POST', 'Post'))
518
        );
519
        if ($usePreview) {
520
            $actions->push(
521
                FormAction::create('doPreviewComment', _t('CommentInterface.PREVIEW', 'Preview'))
522
                    ->addExtraClass('action-minor')
523
                    ->setAttribute('style', 'display: none') // enable through JS
524
            );
525
        }
526
527
        // required fields for server side
528
        $required = new RequiredFields($this->config()->required_fields);
529
530
        // create the comment form
531
        $form = new Form($this, 'CommentsForm', $fields, $actions, $required);
532
533
        // if the record exists load the extra required data
534
        if ($record = $this->getOwnerRecord()) {
535
            // Load member data
536
            $member = Member::currentUser();
537
            if (($record->CommentsRequireLogin || $record->PostingRequiredPermission) && $member) {
538
                $fields = $form->Fields();
539
540
                $fields->removeByName('Name');
541
                $fields->removeByName('Email');
542
                $fields->insertBefore(
543
                    new ReadonlyField(
544
                        'NameView',
545
                        _t('CommentInterface.YOURNAME', 'Your name'),
546
                        $member->getName()
547
                    ),
548
                    'URL'
549
                );
550
                $fields->push(new HiddenField('Name', '', $member->getName()));
551
                $fields->push(new HiddenField('Email', '', $member->Email));
552
            }
553
554
            // we do not want to read a new URL when the form has already been submitted
555
            // which in here, it hasn't been.
556
            $form->loadDataFrom(array(
557
                'ParentID'        => $record->ID,
558
                'ReturnURL'       => $this->request->getURL(),
559
                'ParentClassName' => $this->getParentClass()
560
            ));
561
        }
562
563
        // Set it so the user gets redirected back down to the form upon form fail
564
        $form->setRedirectToFormOnValidationError(true);
565
566
        // load any data from the cookies
567
        if ($data = Cookie::get('CommentsForm_UserData')) {
568
            $data = Convert::json2array($data);
569
570
            $form->loadDataFrom(array(
571
                'Name'  => isset($data['Name']) ? $data['Name'] : '',
572
                'URL'   => isset($data['URL']) ? $data['URL'] : '',
573
                'Email' => isset($data['Email']) ? $data['Email'] : ''
574
            ));
575
            // allow previous value to fill if comment not stored in cookie (i.e. validation error)
576
            $prevComment = Cookie::get('CommentsForm_Comment');
577
            if ($prevComment && $prevComment != '') {
578
                $form->loadDataFrom(array('Comment' => $prevComment));
579
            }
580
        }
581
582
        if (!empty($member)) {
583
            $form->loadDataFrom($member);
584
        }
585
586
        // hook to allow further extensions to alter the comments form
587
        $this->extend('alterCommentForm', $form);
588
589
        return $form;
590
    }
591
592
    /**
593
     * Process which creates a {@link Comment} once a user submits a comment from this form.
594
     *
595
     * @param  array $data
596
     * @param  Form $form
597
     * @return HTTPResponse
598
     */
599
    public function doPostComment($data, $form)
600
    {
601
        // Load class and parent from data
602
        if (isset($data['ParentClassName'])) {
603
            $this->setParentClass($data['ParentClassName']);
604
        }
605
        if (isset($data['ParentID']) && ($class = $this->getParentClass())) {
606
            $this->setOwnerRecord($class::get()->byID($data['ParentID']));
607
        }
608
        if (!$this->getOwnerRecord()) {
609
            return $this->httpError(404);
610
        }
611
612
        // cache users data
613
        Cookie::set('CommentsForm_UserData', Convert::raw2json($data));
614
        Cookie::set('CommentsForm_Comment', $data['Comment']);
615
616
        // extend hook to allow extensions. Also see onAfterPostComment
617
        $this->extend('onBeforePostComment', $form);
618
619
        // If commenting can only be done by logged in users, make sure the user is logged in
620
        if (!$this->getOwnerRecord()->canPostComment()) {
621
            return Security::permissionFailure(
622
                $this,
623
                _t(
624
                    'CommentingController.PERMISSIONFAILURE',
625
                    "You're not able to post comments to this page. Please ensure you are logged in and have an "
626
                    . 'appropriate permission level.'
627
                )
628
            );
629
        }
630
631
        if ($member = Member::currentUser()) {
632
            $form->Fields()->push(new HiddenField('AuthorID', 'Author ID', $member->ID));
633
        }
634
635
        // What kind of moderation is required?
636
        switch ($this->getOwnerRecord()->ModerationRequired) {
637
            case 'Required':
638
                $requireModeration = true;
639
                break;
640
            case 'NonMembersOnly':
641
                $requireModeration = empty($member);
642
                break;
643
            case 'None':
644
            default:
645
                $requireModeration = false;
646
                break;
647
        }
648
649
        $comment = new Comment();
650
        $form->saveInto($comment);
651
652
        $comment->ParentID = $data['ParentID'];
653
        $comment->ParentClass = $data['ParentClassName'];
654
655
        $comment->AllowHtml = $this->getOption('html_allowed');
656
        $comment->Moderated = !$requireModeration;
657
658
        // Save into DB, or call pre-save hooks to give accurate preview
659
        $usePreview = $this->getOption('use_preview');
660
        $isPreview = $usePreview && !empty($data['IsPreview']);
661
        if ($isPreview) {
662
            $comment->extend('onBeforeWrite');
663
        } else {
664
            $comment->write();
665
666
            // extend hook to allow extensions. Also see onBeforePostComment
667
            $this->extend('onAfterPostComment', $comment);
668
        }
669
670
        // we want to show a notification if comments are moderated
671
        if ($requireModeration && !$comment->IsSpam) {
672
            Session::set('CommentsModerated', 1);
673
        }
674
675
        // clear the users comment since it passed validation
676
        Cookie::set('CommentsForm_Comment', false);
677
678
        // Find parent link
679
        if (!empty($data['ReturnURL'])) {
680
            $url = $data['ReturnURL'];
681
        } elseif ($parent = $comment->Parent()) {
682
            $url = $parent->Link();
683
        } else {
684
            return $this->redirectBack();
685
        }
686
687
        // Given a redirect page exists, attempt to link to the correct anchor
688
        if ($comment->IsSpam) {
689
            // Link to the form with the error message contained
690
            $hash = $form->FormName();
691
        } elseif (!$comment->Moderated) {
692
            // Display the "awaiting moderation" text
693
            $hash = 'moderated';
694
        } else {
695
            // Link to the moderated, non-spam comment
696
            $hash = $comment->Permalink();
697
        }
698
699
        return $this->redirect(Controller::join_links($url, "#{$hash}"));
700
    }
701
702
    /**
703
     * @param  array $data
704
     * @param  Form $form
705
     * @return HTTPResponse
706
     */
707
    public function doPreviewComment($data, $form)
708
    {
709
        $data['IsPreview'] = 1;
710
711
        return $this->doPostComment($data, $form);
712
    }
713
714
    /**
715
     * @return HTTPResponse|false
716
     */
717
    public function redirectBack()
718
    {
719
        // Don't cache the redirect back ever
720
        HTTP::set_cache_age(0);
721
722
        $url = null;
723
724
        // In edge-cases, this will be called outside of a handleRequest() context; in that case,
725
        // redirect to the homepage - don't break into the global state at this stage because we'll
726
        // be calling from a test context or something else where the global state is inappropraite
727
        if ($this->request) {
728
            if ($this->request->requestVar('BackURL')) {
729
                $url = $this->request->requestVar('BackURL');
730
            } elseif ($this->request->isAjax() && $this->request->getHeader('X-Backurl')) {
731
                $url = $this->request->getHeader('X-Backurl');
732
            } elseif ($this->request->getHeader('Referer')) {
733
                $url = $this->request->getHeader('Referer');
734
            }
735
        }
736
737
        if (!$url) {
738
            $url = $this->fallbackReturnURL;
739
        }
740
        if (!$url) {
741
            $url = Director::baseURL();
742
        }
743
744
        // absolute redirection URLs not located on this site may cause phishing
745
        if (Director::is_site_url($url)) {
746
            return $this->redirect($url);
747
        } else {
748
            return false;
749
        }
750
    }
751
}
752