Passed
Pull Request — 4 (#10081)
by Bram
10:29
created

Controller::ContentLocale()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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