Passed
Pull Request — 4 (#9744)
by Garion
07:22
created

Controller::prepareResponse()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 20
nc 6
nop 1
dl 0
loc 32
rs 8.9777
c 1
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Dev\Debug;
7
use SilverStripe\Dev\Deprecation;
8
use SilverStripe\ORM\FieldType\DBHTMLText;
9
use SilverStripe\Security\BasicAuth;
10
use SilverStripe\Security\BasicAuthMiddleware;
11
use SilverStripe\Security\Member;
12
use SilverStripe\Security\Security;
13
use SilverStripe\View\SSViewer;
14
use SilverStripe\View\TemplateGlobalProvider;
15
16
/**
17
 * Controllers are the cornerstone of all site functionality in SilverStripe. The {@link Director}
18
 * selects a controller to pass control to, and then calls {@link handleRequest()}. This method will execute
19
 * the appropriate action - either by calling the action method, or displaying the action's template.
20
 *
21
 * See {@link getTemplate()} for information on how the template is chosen.
22
 */
23
class Controller extends RequestHandler implements TemplateGlobalProvider
24
{
25
26
    /**
27
     * An array of arguments extracted from the URL.
28
     *
29
     * @var array
30
     */
31
    protected $urlParams;
32
33
    /**
34
     * Contains all GET and POST parameters passed to the current {@link HTTPRequest}.
35
     *
36
     * @var array
37
     */
38
    protected $requestParams;
39
40
    /**
41
     * The URL part matched on the current controller as determined by the "$Action" part of the
42
     * {@link $url_handlers} definition. Should correlate to a public method on this controller.
43
     *
44
     * Used in {@link render()} and {@link getViewer()} to determine action-specific templates.
45
     *
46
     * @var string
47
     */
48
    protected $action;
49
50
    /**
51
     * Stack of current controllers. Controller::$controller_stack[0] is the current controller.
52
     *
53
     * @var array
54
     */
55
    protected static $controller_stack = [];
56
57
    /**
58
     * Assign templates for this controller.
59
     * Map of action => template name
60
     *
61
     * @var array
62
     */
63
    protected $templates = [];
64
65
    /**
66
     * @deprecated 4.1.0:5.0.0 Add this controller's url to
67
     * SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false
68
     * @var bool
69
     */
70
    protected $basicAuthEnabled = true;
71
72
    /**
73
     * The response object that the controller returns.
74
     *
75
     * Set in {@link handleRequest()}.
76
     *
77
     * @var HTTPResponse
78
     */
79
    protected $response;
80
81
    /**
82
     * Default URL handlers.
83
     *
84
     * @var array
85
     */
86
    private static $url_handlers = [
0 ignored issues
show
introduced by
The private property $url_handlers is not used, and could be removed.
Loading history...
87
        '$Action//$ID/$OtherID' => 'handleAction',
88
    ];
89
90
    /**
91
     * @var array
92
     */
93
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
94
        'handleAction',
95
        'handleIndex',
96
    ];
97
98
    /**
99
     * Initialisation function that is run before any action on the controller is called.
100
     *
101
     * @uses BasicAuth::requireLogin()
102
     */
103
    protected function init()
104
    {
105
        // @todo This will be removed in 5.0 and will be controlled by middleware instead
106
        if ($this->basicAuthEnabled) {
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Control\Controller::$basicAuthEnabled has been deprecated: 4.1.0:5.0.0 Add this controller's url to SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

106
        if (/** @scrutinizer ignore-deprecated */ $this->basicAuthEnabled) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
107
            BasicAuth::protect_site_if_necessary();
108
        }
109
110
        // This is used to test that subordinate controllers are actually calling parent::init() - a common bug
111
        $this->baseInitCalled = true;
112
    }
113
114
    /**
115
     * A stand in function to protect the init function from failing to be called as well as providing before and
116
     * after hooks for the init function itself
117
     *
118
     * This should be called on all controllers before handling requests
119
     */
120
    public function doInit()
121
    {
122
        //extension hook
123
        $this->extend('onBeforeInit');
124
125
        // Safety call
126
        $this->baseInitCalled = false;
127
        $this->init();
128
        if (!$this->baseInitCalled) {
129
            $class = static::class;
130
            user_error(
131
                "init() method on class '{$class}' doesn't call Controller::init()."
132
                . "Make sure that you have parent::init() included.",
133
                E_USER_WARNING
134
            );
135
        }
136
137
        $this->extend('onAfterInit');
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     *
143
     * Also set the URLParams
144
     */
145
    public function setRequest($request)
146
    {
147
        $return = parent::setRequest($request);
148
        $this->setURLParams($this->getRequest()->allParams());
149
150
        return $return;
151
    }
152
153
    /**
154
     * A bootstrap for the handleRequest method
155
     *
156
     * @todo setDataModel and setRequest are redundantly called in parent::handleRequest() - sort this out
157
     *
158
     * @param HTTPRequest $request
159
     */
160
    protected function beforeHandleRequest(HTTPRequest $request)
161
    {
162
        //Set up the internal dependencies (request, response)
163
        $this->setRequest($request);
164
        //Push the current controller to protect against weird session issues
165
        $this->pushCurrent();
166
        $this->setResponse(new HTTPResponse());
167
        //kick off the init functionality
168
        $this->doInit();
169
    }
170
171
    /**
172
     * Cleanup for the handleRequest method
173
     */
174
    protected function afterHandleRequest()
175
    {
176
        //Pop the current controller from the stack
177
        $this->popCurrent();
178
    }
179
180
    /**
181
     * Executes this controller, and return an {@link HTTPResponse} object with the result.
182
     *
183
     * This method defers to {@link RequestHandler->handleRequest()} to determine which action
184
     *    should be executed
185
     *
186
     * Note: You should rarely need to overload handleRequest() -
187
     * this kind of change is only really appropriate for things like nested
188
     * controllers - {@link ModelAsController} and {@link RootURLController}
189
     * are two examples here.  If you want to make more
190
     * orthodox functionality, it's better to overload {@link init()} or {@link index()}.
191
     *
192
     * Important: If you are going to overload handleRequest,
193
     * make sure that you start the method with $this->beforeHandleRequest()
194
     * and end the method with $this->afterHandleRequest()
195
     *
196
     * @param HTTPRequest $request
197
     * @return HTTPResponse
198
     */
199
    public function handleRequest(HTTPRequest $request)
200
    {
201
        if (!$request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
202
            throw new \RuntimeException('Controller::handleRequest() not passed a request!');
203
        }
204
205
        //set up the controller for the incoming request
206
        $this->beforeHandleRequest($request);
207
208
        //if the before handler manipulated the response in a way that we shouldn't proceed, then skip our request
209
        // handling
210
        if (!$this->getResponse()->isFinished()) {
211
            //retrieve the response for the request
212
            $response = parent::handleRequest($request);
213
214
            //prepare the response (we can receive an assortment of response types (strings/objects/HTTPResponses)
215
            $this->prepareResponse($response);
216
        }
217
218
        //after request work
219
        $this->afterHandleRequest();
220
221
        //return the response
222
        return $this->getResponse();
223
    }
224
225
    /**
226
     * Prepare the response (we can receive an assortment of response types (strings/objects/HTTPResponses) and
227
     * changes the controller response object appropriately
228
     *
229
     * @param HTTPResponse|Object $response
230
     */
231
    protected function prepareResponse($response)
232
    {
233
        if (!is_object($response)) {
234
            $this->getResponse()->setBody($response);
235
        } elseif ($response instanceof HTTPResponse) {
236
            if (isset($_REQUEST['debug_request'])) {
237
                $class = static::class;
238
                Debug::message(
239
                    "Request handler returned HTTPResponse object to {$class} controller;"
240
                    . "returning it without modification."
241
                );
242
            }
243
            $this->setResponse($response);
244
        } else {
245
            // Could be Controller, or ViewableData_Customised controller wrapper
246
            if (ClassInfo::hasMethod($response, 'getViewer')) {
247
                if (isset($_REQUEST['debug_request'])) {
248
                    $class = static::class;
249
                    $responseClass = get_class($response);
250
                    Debug::message(
251
                        "Request handler {$responseClass} object to {$class} controller;"
252
                        . "rendering with template returned by {$responseClass}::getViewer()"
253
                    );
254
                }
255
                $response = $response->getViewer($this->getAction())->process($response);
256
            }
257
258
            $this->getResponse()->setBody($response);
259
        }
260
261
        //deal with content if appropriate
262
        ContentNegotiator::process($this->getResponse());
263
    }
264
265
    /**
266
     * Controller's default action handler.  It will call the method named in "$Action", if that method
267
     * exists. If "$Action" isn't given, it will use "index" as a default.
268
     *
269
     * @param HTTPRequest $request
270
     * @param string $action
271
     *
272
     * @return DBHTMLText|HTTPResponse
273
     */
274
    protected function handleAction($request, $action)
275
    {
276
        foreach ($request->latestParams() as $k => $v) {
277
            if ($v || !isset($this->urlParams[$k])) {
278
                $this->urlParams[$k] = $v;
279
            }
280
        }
281
282
        $this->action = $action;
283
        $this->requestParams = $request->requestVars();
284
285
        if ($this->hasMethod($action)) {
286
            $result = parent::handleAction($request, $action);
287
288
            // If the action returns an array, customise with it before rendering the template.
289
            if (is_array($result)) {
0 ignored issues
show
introduced by
The condition is_array($result) is always false.
Loading history...
290
                return $this->getViewer($action)->process($this->customise($result));
291
            } else {
292
                return $result;
293
            }
294
        }
295
296
        // Fall back to index action with before/after handlers
297
        $beforeResult = $this->extend('beforeCallActionHandler', $request, $action);
298
        if ($beforeResult) {
299
            return reset($beforeResult);
300
        }
301
302
        $result = $this->getViewer($action)->process($this);
303
304
        $afterResult = $this->extend('afterCallActionHandler', $request, $action, $result);
305
        if ($afterResult) {
306
            return reset($afterResult);
307
        }
308
309
        return $result;
310
    }
311
312
    /**
313
     * @param array $urlParams
314
     * @return $this
315
     */
316
    public function setURLParams($urlParams)
317
    {
318
        $this->urlParams = $urlParams;
319
        return $this;
320
    }
321
322
    /**
323
     * Returns the parameters extracted from the URL by the {@link Director}.
324
     *
325
     * @return array
326
     */
327
    public function getURLParams()
328
    {
329
        return $this->urlParams;
330
    }
331
332
    /**
333
     * Returns the HTTPResponse object that this controller is building up. Can be used to set the
334
     * status code and headers.
335
     *
336
     * @return HTTPResponse
337
     */
338
    public function getResponse()
339
    {
340
        if (!$this->response) {
341
            $this->setResponse(new HTTPResponse());
342
        }
343
        return $this->response;
344
    }
345
346
    /**
347
     * Sets the HTTPResponse object that this controller is building up.
348
     *
349
     * @param HTTPResponse $response
350
     *
351
     * @return $this
352
     */
353
    public function setResponse(HTTPResponse $response)
354
    {
355
        $this->response = $response;
356
        return $this;
357
    }
358
359
    /**
360
     * @var bool
361
     */
362
    protected $baseInitCalled = false;
363
364
    /**
365
     * This is the default action handler used if a method doesn't exist. It will process the
366
     * controller object with the template returned by {@link getViewer()}.
367
     *
368
     * @param string $action
369
     * @return DBHTMLText
370
     */
371
    public function defaultAction($action)
372
    {
373
        return $this->getViewer($action)->process($this);
374
    }
375
376
    /**
377
     * Returns the action that is being executed on this controller.
378
     *
379
     * @return string
380
     */
381
    public function getAction()
382
    {
383
        return $this->action;
384
    }
385
386
    /**
387
     * Return the viewer identified being the default handler for this Controller/Action combination.
388
     *
389
     * @param string $action
390
     *
391
     * @return SSViewer
392
     */
393
    public function getViewer($action)
394
    {
395
        // Hard-coded templates
396
        if (isset($this->templates[$action]) && $this->templates[$action]) {
397
            $templates = $this->templates[$action];
398
        } elseif (isset($this->templates['index']) && $this->templates['index']) {
399
            $templates = $this->templates['index'];
400
        } elseif ($this->template) {
0 ignored issues
show
Bug Best Practice introduced by
The property template does not exist on SilverStripe\Control\Controller. Since you implemented __get, consider adding a @property annotation.
Loading history...
401
            $templates = $this->template;
402
        } else {
403
            // Build templates based on class hierarchy
404
            $actionTemplates = [];
405
            $classTemplates = [];
406
            $parentClass = static::class;
407
            while ($parentClass !== parent::class) {
408
                // _action templates have higher priority
409
                if ($action && $action != 'index') {
410
                    $actionTemplates[] = strtok($parentClass, '_') . '_' . $action;
411
                }
412
                // class templates have lower priority
413
                $classTemplates[] = strtok($parentClass, '_');
414
                $parentClass = get_parent_class($parentClass);
415
            }
416
417
            // Add controller templates for inheritance chain
418
            $templates = array_unique(array_merge($actionTemplates, $classTemplates));
419
        }
420
421
        return SSViewer::create($templates);
422
    }
423
424
    /**
425
     * @param string $action
426
     *
427
     * @return bool
428
     */
429
    public function hasAction($action)
430
    {
431
        return parent::hasAction($action) || $this->hasActionTemplate($action);
432
    }
433
434
    /**
435
     * Removes all the "action" part of the current URL and returns the result. If no action parameter
436
     * is present, returns the full URL.
437
     *
438
     * @param string $fullURL
439
     * @param null|string $action
440
     *
441
     * @return string
442
     */
443
    public function removeAction($fullURL, $action = null)
444
    {
445
        if (!$action) {
446
            $action = $this->getAction();    //default to current action
447
        }
448
        $returnURL = $fullURL;
449
450
        if (($pos = strpos($fullURL, $action)) !== false) {
451
            $returnURL = substr($fullURL, 0, $pos);
452
        }
453
454
        return $returnURL;
455
    }
456
457
    /**
458
     * Return the class that defines the given action, so that we know where to check allowed_actions.
459
     * Overrides RequestHandler to also look at defined templates.
460
     *
461
     * @param string $action
462
     *
463
     * @return string
464
     */
465
    protected function definingClassForAction($action)
466
    {
467
        $definingClass = parent::definingClassForAction($action);
468
        if ($definingClass) {
469
            return $definingClass;
470
        }
471
472
        $class = static::class;
473
        while ($class != 'SilverStripe\\Control\\RequestHandler') {
474
            $templateName = strtok($class, '_') . '_' . $action;
475
            if (SSViewer::hasTemplate($templateName)) {
476
                return $class;
477
            }
478
479
            $class = get_parent_class($class);
480
        }
481
482
        return null;
483
    }
484
485
    /**
486
     * Returns TRUE if this controller has a template that is specifically designed to handle a
487
     * specific action.
488
     *
489
     * @param string $action
490
     *
491
     * @return bool
492
     */
493
    public function hasActionTemplate($action)
494
    {
495
        if (isset($this->templates[$action])) {
496
            return true;
497
        }
498
499
        $parentClass = static::class;
500
        $templates   = [];
501
502
        while ($parentClass != __CLASS__) {
503
            $templates[] = strtok($parentClass, '_') . '_' . $action;
504
            $parentClass = get_parent_class($parentClass);
505
        }
506
507
        return SSViewer::hasTemplate($templates);
508
    }
509
510
    /**
511
     * Render the current controller with the templates determined by {@link getViewer()}.
512
     *
513
     * @param array $params
514
     *
515
     * @return string
516
     */
517
    public function render($params = null)
518
    {
519
        $template = $this->getViewer($this->getAction());
520
521
        // if the object is already customised (e.g. through Controller->run()), use it
522
        $obj = $this->getCustomisedObj() ?: $this;
523
524
        if ($params) {
525
            $obj = $this->customise($params);
526
        }
527
528
        return $template->process($obj);
529
    }
530
531
    /**
532
     * Call this to disable site-wide basic authentication for a specific controller. This must be
533
     * called before Controller::init(). That is, you must call it in your controller's init method
534
     * before it calls parent::init().
535
     *
536
     * @deprecated 4.1.0:5.0.0 Add this controller's url to
537
     * SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false
538
     */
539
    public function disableBasicAuth()
540
    {
541
        Deprecation::notice(
542
            '5.0',
543
            'Add this controller\'s url to ' . BasicAuthMiddleware::class . '.URLPatterns injected property instead'
544
        );
545
        $this->basicAuthEnabled = false;
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Control\Controller::$basicAuthEnabled has been deprecated: 4.1.0:5.0.0 Add this controller's url to SilverStripe\Security\BasicAuthMiddleware.URLPatterns injected property instead of setting false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

545
        /** @scrutinizer ignore-deprecated */ $this->basicAuthEnabled = false;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
546
    }
547
548
    /**
549
     * Returns the current controller.
550
     *
551
     * @return Controller
552
     */
553
    public static function curr()
554
    {
555
        if (Controller::$controller_stack) {
0 ignored issues
show
Bug Best Practice introduced by
The expression SilverStripe\Control\Controller::controller_stack 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...
556
            return Controller::$controller_stack[0];
557
        }
558
        user_error("No current controller available", E_USER_WARNING);
559
        return null;
560
    }
561
562
    /**
563
     * Tests whether we have a currently active controller or not. True if there is at least 1
564
     * controller in the stack.
565
     *
566
     * @return bool
567
     */
568
    public static function has_curr()
569
    {
570
        return Controller::$controller_stack ? true : false;
571
    }
572
573
    /**
574
     * Returns true if the member is allowed to do the given action. Defaults to the currently logged
575
     * in user.
576
     *
577
     * @param string $perm
578
     * @param null|member $member
579
     *
580
     * @return bool
581
     */
582
    public function can($perm, $member = null)
583
    {
584
        if (!$member) {
585
            $member = Security::getCurrentUser();
586
        }
587
        if (is_array($perm)) {
0 ignored issues
show
introduced by
The condition is_array($perm) is always false.
Loading history...
588
            $perm = array_map([$this, 'can'], $perm, array_fill(0, count($perm), $member));
589
            return min($perm);
590
        }
591
        if ($this->hasMethod($methodName = 'can' . $perm)) {
592
            return $this->$methodName($member);
593
        } else {
594
            return true;
595
        }
596
    }
597
598
    /**
599
     * Pushes this controller onto the stack of current controllers. This means that any redirection,
600
     * session setting, or other things that rely on Controller::curr() will now write to this
601
     * controller object.
602
     *
603
     * Note: Ensure this controller is assigned a request with a valid session before pushing
604
     * it to the stack.
605
     */
606
    public function pushCurrent()
607
    {
608
        // Ensure this controller has a valid session
609
        $this->getRequest()->getSession();
610
        array_unshift(self::$controller_stack, $this);
611
    }
612
613
    /**
614
     * Pop this controller off the top of the stack.
615
     */
616
    public function popCurrent()
617
    {
618
        if ($this === self::$controller_stack[0]) {
619
            array_shift(self::$controller_stack);
620
        } else {
621
            $class = static::class;
622
            user_error(
623
                "popCurrent called on {$class} controller, but it wasn't at the top of the stack",
624
                E_USER_WARNING
625
            );
626
        }
627
    }
628
629
    /**
630
     * Redirect to the given URL.
631
     *
632
     * @param string $url
633
     * @param int $code
634
     * @return HTTPResponse
635
     */
636
    public function redirect($url, $code = 302)
637
    {
638
        if ($this->getResponse()->getHeader('Location') && $this->getResponse()->getHeader('Location') != $url) {
639
            user_error("Already directed to " . $this->getResponse()->getHeader('Location')
640
                . "; now trying to direct to $url", E_USER_WARNING);
641
            return null;
642
        }
643
        $response = parent::redirect($url, $code);
644
        $this->setResponse($response);
645
        return $response;
646
    }
647
648
    /**
649
     * Tests whether a redirection has been requested. If redirect() has been called, it will return
650
     * the URL redirected to. Otherwise, it will return null.
651
     *
652
     * @return null|string
653
     */
654
    public function redirectedTo()
655
    {
656
        return $this->getResponse() && $this->getResponse()->getHeader('Location');
657
    }
658
659
    /**
660
     * Joins two or more link segments together, putting a slash between them if necessary. Use this
661
     * for building the results of {@link Link()} methods. If either of the links have query strings,
662
     * then they will be combined and put at the end of the resulting url.
663
     *
664
     * Caution: All parameters are expected to be URI-encoded already.
665
     *
666
     * @param string|array $arg,.. One or more link segments, or list of link segments as an array
667
     * @return string
668
     */
669
    public static function join_links($arg = null)
670
    {
671
        if (func_num_args() === 1 && is_array($arg)) {
672
            $args = $arg;
673
        } else {
674
            $args = func_get_args();
675
        }
676
        $result = "";
677
        $queryargs = [];
678
        $fragmentIdentifier = null;
679
680
        foreach ($args as $arg) {
681
            // Find fragment identifier - keep the last one
682
            if (strpos($arg, '#') !== false) {
683
                list($arg, $fragmentIdentifier) = explode('#', $arg, 2);
684
            }
685
            // Find querystrings
686
            if (strpos($arg, '?') !== false) {
687
                list($arg, $suffix) = explode('?', $arg, 2);
688
                parse_str($suffix, $localargs);
689
                $queryargs = array_merge($queryargs, $localargs);
690
            }
691
            if ((is_string($arg) && $arg) || is_numeric($arg)) {
692
                $arg = (string) $arg;
693
                if ($result && substr($result, -1) != '/' && $arg[0] != '/') {
694
                    $result .= "/$arg";
695
                } else {
696
                    $result .= (substr($result, -1) == '/' && $arg[0] == '/') ? ltrim($arg, '/') : $arg;
697
                }
698
            }
699
        }
700
701
        if ($queryargs) {
702
            $result .= '?' . http_build_query($queryargs);
703
        }
704
705
        if ($fragmentIdentifier) {
706
            $result .= "#$fragmentIdentifier";
707
        }
708
709
        return $result;
710
    }
711
712
    /**
713
     * @return array
714
     */
715
    public static function get_template_global_variables()
716
    {
717
        return [
718
            'CurrentPage' => 'curr',
719
        ];
720
    }
721
}
722