Version: current

      1 <?php
      2 /**
      3  * Base controller class.
      4  * Controllers are the cornerstone of all site functionality in Sapphire.  The {@link Director}
      5  * selects a controller to pass control to, and then calls {@link run()}.  This method will execute
      6  * the appropriate action - either by calling the action method, or displaying the action's template.
      7  *
      8  * See {@link getTemplate()} for information on how the template is chosen.
      9  * @package sapphire
     10  * @subpackage control
     11  */
     12 class Controller extends RequestHandler {
     13
     14     /**
     15      * @var array $urlParams An array of arguments extracted from the URL
     16      */
     17     protected $urlParams;
     18
     19     /**
     20      * @var array $requestParams Contains all GET and POST parameters
     21      * passed to the current {@link SS_HTTPRequest}.
     22      * @uses SS_HTTPRequest->requestVars()
     23      */
     24     protected $requestParams;
     25
     26     /**
     27      * @var string $action The URL part matched on the current controller as
     28      * determined by the "$Action" part of the {@link $url_handlers} definition.
     29      * Should correlate to a public method on this controller.
     30      * Used in {@link render()} and {@link getViewer()} to determine
     31      * action-specific templates.
     32      */
     33     protected $action;
     34
     35     /**
     36      * The {@link Session} object for this controller
     37      */
     38     protected $session;
     39
     40     /**
     41      * Stack of current controllers.
     42      * Controller::$controller_stack[0] is the current controller.
     43      */
     44     protected static $controller_stack = array();
     45
     46     protected $basicAuthEnabled = true;
     47
     48     /**
     49      * @var SS_HTTPResponse $response The response object that the controller returns.
     50      * Set in {@link handleRequest()}.
     51      */
     52     protected $response;
     53
     54     /**
     55      * @var SS_HTTPRequest $request The request object that the controller was called with.
     56      * Set in {@link handleRequest()}. Useful to generate the {}
     57      */
     58     protected $request;
     59
     60     /**
     61      * Default URL handlers - (Action)/(ID)/(OtherID)
     62      */
     63     static $url_handlers = array(
     64         '$Action//$ID/$OtherID' => 'handleAction',
     65     );
     66
     67     static $allowed_actions = array(
     68         'handleAction',
     69         'handleIndex',
     70     );
     71
     72     /**
     73      * Initialisation function that is run before any action on the controller is called.
     74      *
     75      * @uses BasicAuth::requireLogin()
     76      */
     77     function init() {
     78         if($this->basicAuthEnabled) BasicAuth::protect_site_if_necessary();
     79
     80         // Directly access the session variable just in case the Group or Member tables don't yet exist
     81         if(Session::get('loggedInAs') && Security::database_is_ready()) {
     82             $member = Member::currentUser();
     83             if($member) {
     84                 Cookie::set("PastMember", true, 90, null, null, false, true);
     85                 DB::query("UPDATE \"Member\" SET \"LastVisited\" = " . DB::getConn()->now() . " WHERE \"ID\" = $member->ID", null);
     86             }
     87         }
     88
     89         // This is used to test that subordinate controllers are actually calling parent::init() - a common bug
     90         $this->baseInitCalled = true;
     91     }
     92
     93     /**
     94      * Executes this controller, and return an {@link SS_HTTPResponse} object with the result.
     95      *
     96      * This method first does a few set-up activities:
     97      *  - Push this controller ont to the controller stack -
     98      *    see {@link Controller::curr()} for information about this.
     99      *  - Call {@link init()}
    100      *  - Defer to {@link RequestHandler->handleRequest()} to determine which action
    101      *    should be executed
    102      *
    103      * Note: $requestParams['executeForm'] support was removed,
    104      * make the following change in your URLs:
    105      * "/?executeForm=FooBar" -> "/FooBar"
    106      * Also make sure "FooBar" is in the $allowed_actions of your controller class.
    107      *
    108      * Note: You should rarely need to overload run() -
    109      * this kind of change is only really appropriate for things like nested
    110      * controllers - {@link ModelAsController} and {@link RootURLController}
    111      * are two examples here.  If you want to make more
    112      * orthodox functionality, it's better to overload {@link init()} or {@link index()}.
    113      *
    114      * Important: If you are going to overload handleRequest,
    115      * make sure that you start the method with $this->pushCurrent()
    116      * and end the method with $this->popCurrent().
    117      * Failure to do this will create weird session errors.
    118      *
    119      * @param $request The {@link SS_HTTPRequest} object that is responsible
    120      *  for distributing request parsing.
    121      * @return SS_HTTPResponse The response that this controller produces,
    122      *  including HTTP headers such as redirection info
    123      */
    124     function handleRequest(SS_HTTPRequest $request) {
    125         if(!$request) user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR);
    126
    127         $this->pushCurrent();
    128         $this->urlParams = $request->allParams();
    129         $this->request = $request;
    130         $this->response = new SS_HTTPResponse();
    131
    132         $this->extend('onBeforeInit');
    133
    134         // Init
    135         $this->baseInitCalled = false;
    136         $this->init();
    137         if(!$this->baseInitCalled) user_error("init() method on class '$this->class' doesn't call Controller::init().  Make sure that you have parent::init() included.", E_USER_WARNING);
    138
    139         $this->extend('onAfterInit');
    140
    141         // If we had a redirection or something, halt processing.
    142         if($this->response->isFinished()) {
    143             $this->popCurrent();
    144             return $this->response;
    145         }
    146
    147         $body = parent::handleRequest($request);
    148         if($body instanceof SS_HTTPResponse) {
    149             if(isset($_REQUEST['debug_request'])) Debug::message("Request handler returned SS_HTTPResponse object to $this->class controller; returning it without modification.");
    150             $this->response = $body;
    151
    152         } else {
    153             if(is_object($body)) {
    154                 if(isset($_REQUEST['debug_request'])) Debug::message("Request handler $body->class object to $this->class controller;, rendering with template returned by $body->class::getViewer()");
    155                $body = $body->getViewer($request->latestParam('Action'))->process($body);
    156             }
    157
    158             $this->response->setBody($body);
    159         }
    160
    161
    162         ContentNegotiator::process($this->response);
    163         HTTP::add_cache_headers($this->response);
    164
    165         $this->popCurrent();
    166         return $this->response;
    167     }
    168
    169     /**
    170      * Controller's default action handler.  It will call the method named in $Action, if that method exists.
    171      * If $Action isn't given, it will use "index" as a default.
    172      */
    173     public function handleAction($request) {
    174         // urlParams, requestParams, and action are set for backward compatability
    175         foreach($request->latestParams() as $k => $v) {
    176             if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
    177         }
    178
    179         $this->action = str_replace("-","_",$request->param('Action'));
    180         $this->requestParams = $request->requestVars();
    181         if(!$this->action) $this->action = 'index';
    182
    183         if(!$this->hasAction($this->action)) {
    184             $this->httpError(404, "The action '$this->action' does not exist in class $this->class");
    185         }
    186
    187         // run & init are manually disabled, because they create infinite loops and other dodgy situations
    188         if(!$this->checkAccessAction($this->action) || in_array(strtolower($this->action), array('run', 'init'))) {
    189             return $this->httpError(403, "Action '$this->action' isn't allowed on class $this->class");
    190         }
    191
    192         if($this->hasMethod($this->action)) {
    193             $result = $this->{$this->action}($request);
    194
    195             // If the action returns an array, customise with it before rendering the template.
    196             if(is_array($result)) {
    197                 return $this->getViewer($this->action)->process($this->customise($result));
    198             } else {
    199                 return $result;
    200             }
    201         } else {
    202             return $this->getViewer($this->action)->process($this);
    203         }
    204     }
    205
    206     function setURLParams($urlParams) {
    207         $this->urlParams = $urlParams;
    208     }
    209
    210     /**
    211      * @return array The parameters extracted from the URL by the {@link Director}.
    212      */
    213     function getURLParams() {
    214         return $this->urlParams;
    215     }
    216
    217     /**
    218      * Returns the SS_HTTPResponse object that this controller is building up.
    219      * Can be used to set the status code and headers
    220      */
    221     function getResponse() {
    222         return $this->response;
    223     }
    224
    225     /**
    226      * Get the request with which this controller was called (if any).
    227      * Usually set in {@link handleRequest()}.
    228      *
    229      * @return SS_HTTPRequest
    230      */
    231     function getRequest() {
    232         return $this->request;
    233     }
    234
    235     protected $baseInitCalled = false;
    236
    237     /**
    238      * Return the object that is going to own a form that's being processed, and handle its execution.
    239      * Note that the result needn't be an actual controller object.
    240      */
    241     function getFormOwner() {
    242         // Get the appropraite ocntroller: sometimes we want to get a form from another controller
    243         if(isset($this->requestParams['formController'])) {
    244             $formController = Director::getControllerForURL($this->requestParams['formController']);
    245
    246             while(is_a($formController, 'NestedController')) {
    247                 $formController = $formController->getNestedController();
    248             }
    249             return $formController;
    250
    251         } else {
    252             return $this;
    253         }
    254     }
    255
    256     /**
    257      * This is the default action handler used if a method doesn't exist.
    258      * It will process the controller object with the template returned by {@link getViewer()}
    259      */
    260     function defaultAction($action) {
    261         return $this->getViewer($action)->process($this);
    262     }
    263
    264     /**
    265      * Returns the action that is being executed on this controller.
    266      */
    267     function getAction() {
    268         return $this->action;
    269     }
    270
    271     /**
    272      * Return an SSViewer object to process the data
    273      * @return SSViewer The viewer identified being the default handler for this Controller/Action combination
    274      */
    275     function getViewer($action) {
    276         // Hard-coded templates
    277         if($this->templates[$action]) {
    278             $templates = $this->templates[$action];
    279         }   else if($this->templates['index']) {
    280             $templates = $this->templates['index'];
    281         }   else if($this->template) {
    282             $templates = $this->template;
    283         } else {
    284             // Add action-specific templates for inheritance chain
    285             $parentClass = $this->class;
    286             if($action && $action != 'index') {
    287                 $parentClass = $this->class;
    288                 while($parentClass != "Controller") {
    289                     $template = strtok($parentClass,'_') . '_' . $action;
    290                     if ($this->isAjax()) {
    291                         $templates[] = $template . '_ajax';
    292                     }
    293                     $templates[] = $template;
    294                     $parentClass = get_parent_class($parentClass);
    295                 }
    296             }
    297             // Add controller templates for inheritance chain
    298             $parentClass = $this->class;
    299             while($parentClass != "Controller") {
    300                 $template = strtok($parentClass,'_');
    301                 if ($this->isAjax()) {
    302                     $templates[] = $template . '_ajax';
    303                 }
    304                 $templates[] = $template;
    305                 $parentClass = get_parent_class($parentClass);
    306             }
    307
    308             // remove duplicates
    309             $templates = array_unique($templates);
    310         }
    311         return new SSViewer($templates);
    312     }
    313
    314     public function hasAction($action) {
    315         return parent::hasAction($action) || $this->hasActionTemplate($action);
    316     }
    317
    318     /**
    319      * Returns TRUE if this controller has a template that is specifically designed to handle a specific action.
    320      *
    321      * @param string $action
    322      * @return bool
    323      */
    324     public function hasActionTemplate($action) {
    325         if(isset($this->templates[$action])) return true;
    326
    327         $parentClass = $this->class;
    328         $templates   = array();
    329
    330         while($parentClass != 'Controller') {
    331             $templates[] = strtok($parentClass, '_') . '_' . $action;
    332             $parentClass = get_parent_class($parentClass);
    333         }
    334
    335         return SSViewer::hasTemplate($templates);
    336     }
    337
    338     /**
    339      * Render the current controller with the templates determined
    340      * by {@link getViewer()}.
    341      *
    342      * @param array $params Key-value array for custom template variables (Optional)
    343      * @return string Parsed template content
    344      */
    345     function render($params = null) {
    346         $template = $this->getViewer($this->getAction());
    347
    348         // if the object is already customised (e.g. through Controller->run()), use it
    349         $obj = ($this->customisedObj) ? $this->customisedObj : $this;
    350
    351         if($params) $obj = $this->customise($params);
    352
    353         return $template->process($obj);
    354     }
    355
    356     /**
    357      * Call this to disable site-wide basic authentication for a specific contoller.
    358      * This must be called before Controller::init().  That is, you must call it in your controller's
    359      * init method before it calls parent::init().
    360      */
    361     function disableBasicAuth() {
    362         $this->basicAuthEnabled = false;
    363     }
    364
    365     /**
    366      * Returns the current controller
    367      * @returns Controller
    368      */
    369     public static function curr() {
    370         if(Controller::$controller_stack) {
    371             return Controller::$controller_stack[0];
    372         } else {
    373             user_error("No current controller available", E_USER_WARNING);
    374         }
    375     }
    376
    377     /**
    378      * Tests whether we have a currently active controller or not
    379      * @return boolean True if there is at least 1 controller in the stack.
    380      */
    381     public static function has_curr() {
    382         return Controller::$controller_stack ? true : false;
    383     }
    384
    385     /**
    386      * Returns true if the member is allowed to do the given action.
    387      * @param perm The permission to be checked, such as 'View'.
    388      * @param member The member whose permissions need checking.  Defaults to the currently logged
    389      * in user.
    390      * @return boolean
    391      */
    392     function can($perm, $member = null) {
    393         if(!$member) $member = Member::currentUser();
    394         if($this->hasMethod($methodName = 'can' . $perm)) {
    395             return $this->$methodName($member);
    396         } else {
    397             return true;
    398         }
    399     }
    400
    401     //-----------------------------------------------------------------------------------
    402
    403     /**
    404      * returns a date object for use within a template
    405      * Usage: $Now.Year - Returns 2006
    406      * @return Date The current date
    407      */
    408     function Now() {
    409         $d = new Date(null);
    410         $d->setValue(date("Y-m-d h:i:s"));
    411         return $d;
    412     }
    413
    414     /**
    415      * Returns the currently logged in user
    416      */
    417     function CurrentMember() {
    418         return Member::currentUser();
    419     }
    420
    421     /**
    422      * Returns true if the visitor has been here before
    423      * @return boolean
    424      */
    425     function PastVisitor() {
    426         user_error("Controller::PastVisitor() is deprecated", E_USER_NOTICE);
    427         return false;
    428     }
    429
    430     /**
    431      * Return true if the visitor has signed up for a login account before
    432      * @return boolean
    433      */
    434     function PastMember() {
    435         return Cookie::get("PastMember") ? true : false;
    436     }
    437
    438     /**
    439      * Pushes this controller onto the stack of current controllers.
    440      * This means that any redirection, session setting, or other things that rely on Controller::curr() will now write to this
    441      * controller object.
    442      */
    443     function pushCurrent() {
    444         array_unshift(self::$controller_stack, $this);
    445         // Create a new session object
    446         if(!$this->session) {
    447             if(isset(self::$controller_stack[1])) {
    448                 $this->session = self::$controller_stack[1]->getSession();
    449             } else {
    450                 $this->session = new Session(Session::get_all());
    451             }
    452         }
    453     }
    454
    455     /**
    456      * Pop this controller off the top of the stack.
    457      */
    458     function popCurrent() {
    459         if($this === self::$controller_stack[0]) {
    460             array_shift(self::$controller_stack);
    461         } else {
    462             user_error("popCurrent called on $this->class controller, but it wasn't at the top of the stack", E_USER_WARNING);
    463         }
    464     }
    465
    466     /**
    467      * Redirct to the given URL.
    468      * It is generally recommended to call Director::redirect() rather than calling this function directly.
    469      */
    470     function redirect($url, $code=302) {
    471         if($this->response->getHeader('Location')) {
    472             user_error("Already directed to " . $this->response->getHeader('Location') . "; now trying to direct to $url", E_USER_WARNING);
    473             return;
    474         }
    475
    476         // Attach site-root to relative links, if they have a slash in them
    477         if($url == "" || $url[0] == '?' || (substr($url,0,4) != "http" && $url[0] != "/" && strpos($url,'/') !== false)){
    478             $url = Director::baseURL() . $url;
    479         }
    480
    481         $this->response->redirect($url, $code);
    482     }
    483
    484     /**
    485      * Redirect back. Uses either the HTTP_REFERER or a manually set request-variable called
    486      * _REDIRECT_BACK_URL.
    487      * This variable is needed in scenarios where not HTTP-Referer is sent (
    488      * e.g when calling a page by location.href in IE).
    489      * If none of the two variables is available, it will redirect to the base
    490      * URL (see {@link Director::baseURL()}).
    491      * @uses redirect()
    492      */
    493     function redirectBack() {
    494         if($this->request->requestVar('_REDIRECT_BACK_URL')) {
    495             $url = $this->request->requestVar('_REDIRECT_BACK_URL');
    496         } else if($this->request->getHeader('Referer')) {
    497             $url = $this->request->getHeader('Referer');
    498         } else {
    499             $url = Director::baseURL();
    500         }
    501
    502         // absolute redirection URLs not located on this site may cause phishing
    503         if(Director::is_site_url($url)) {
    504             return $this->redirect($url);
    505         } else {
    506             return false;
    507         }
    508
    509     }
    510
    511     /**
    512      * Tests whether a redirection has been requested.
    513      * @return string If redirect() has been called, it will return the URL redirected to.  Otherwise, it will return null;
    514      */
    515     function redirectedTo() {
    516         return $this->response->getHeader('Location');
    517     }
    518
    519     /**
    520      * Get the Session object representing this Controller's session
    521      * @return Session
    522      */
    523     function getSession() {
    524         return $this->session;
    525     }
    526
    527     /**
    528      * Set the Session object.
    529      */
    530     function setSession(Session $session) {
    531         $this->session = $session;
    532     }
    533
    534     /**
    535      * Returns true if this controller is processing an ajax request
    536      * @return boolean True if this controller is processing an ajax request
    537      */
    538     function isAjax() {
    539         return (
    540             isset($this->requestParams['ajax']) || isset($_REQUEST['ajax']) ||
    541             (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
    542         );
    543     }
    544
    545     /**
    546      * Joins two or more link segments together, putting a slash between them if necessary.
    547      * Use this for building the results of {@link Link()} methods.
    548      * If either of the links have query strings,
    549      * then they will be combined and put at the end of the resulting url.
    550      *
    551      * Caution: All parameters are expected to be URI-encoded already.
    552      *
    553      * @param String
    554      * @return String
    555      */
    556     static function join_links() {
    557         $args = func_get_args();
    558         $result = "";
    559         $querystrings = array();
    560         foreach($args as $arg) {
    561             if(strpos($arg,'?') !== false) {
    562                 list($arg, $suffix) = explode('?',$arg,2);
    563                 $querystrings[] = $suffix;
    564             }
    565             if($arg) {
    566                 if($result && substr($result,-1) != '/' && $arg[0] != '/') $result .= "/$arg";
    567                 else $result .= (substr($result, -1) == '/' && $arg[0] == '/') ? ltrim($arg, '/') : $arg;
    568             }
    569         }
    570
    571         if($querystrings) $result .= '?' . implode('&', $querystrings);
    572
    573         return $result;
    574     }
    575 }
    576
    577 ?>
    578

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -