silverstripe-framework/core/control/Controller.php

526 lines
16 KiB
PHP
Raw Normal View History

<?php
/**
* Base controller class.
* Controllers are the cornerstone of all site functionality in Sapphire. The {@link Director}
* selects a controller to pass control to, and then calls {@link run()}. This method will execute
* the appropriate action - either by calling the action method, or displaying the action's template.
*
* See {@link getTemplate()} for information on how the template is chosen.
* @package sapphire
* @subpackage control
*/
class Controller extends RequestHandlingData {
/**
* An array of arguments extracted from the URL
*/
protected $urlParams;
protected $requestParams;
protected $action;
/**
* The {@link Session} object for this controller
*/
protected $session;
/**
* Stack of current controllers.
* Controller::$controller_stack[0] is the current controller.
*/
protected static $controller_stack = array();
protected $basicAuthEnabled = true;
/**
* @var HTTPResponse $response The response object that the controller returns.
* Set in {@link handleRequest()}.
*/
protected $response;
/**
* @var HTTPRequest $request The request object that the controller was called with.
* Set in {@link handleRequest()}. Useful to generate the {}
*/
protected $request;
/**
* Default URL handlers - (Action)/(ID)/(OtherID)
*/
static $url_handlers = array(
'$Action//$ID/$OtherID' => 'handleAction',
);
static $allowed_actions = array(
'handleAction',
'handleIndex',
);
/**
* Handles HTTP requests.
*
* If you are going to overload handleRequest, make sure that you start the method with $this->pushCurrent()
* and end the method with $this->popCurrent(). Failure to do this will create weird session errors.
*
* @param $request The {@link HTTPRequest} object that is responsible for distributing request parsing.
*/
function handleRequest(HTTPRequest $request) {
if(!$request) user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR);
$this->pushCurrent();
$this->urlParams = $request->allParams();
$this->request = $request;
$this->response = new HTTPResponse();
// Init
$this->baseInitCalled = false;
$this->init();
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);
// If we had a redirection or something, halt processing.
if($this->response->isFinished()) {
$this->popCurrent();
return $this->response;
}
$body = parent::handleRequest($request);
if($body instanceof HTTPResponse) {
if(isset($_REQUEST['debug_request'])) Debug::message("Request handler returned HTTPResponse object to $this->class controller; returning it without modification.");
$this->response = $body;
} else {
if(is_object($body)) {
if(isset($_REQUEST['debug_request'])) Debug::message("Request handler $body->class object to $this->class controller;, rendering with template returned by $body->class::getViewer()");
$body = $body->getViewer($request->latestParam('Action'))->process($body);
}
$this->response->setBody($body);
}
ContentNegotiator::process($this->response);
HTTP::add_cache_headers($this->response);
$this->popCurrent();
return $this->response;
}
/**
* Controller's default action handler. It will call the method named in $Action, if that method exists.
* If $Action isn't given, it will use "index" as a default.
*/
function handleAction($request) {
// urlParams, requestParams, and action are set for backward compatability
foreach($request->latestParams() as $k => $v) {
if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
}
$this->action = str_replace("-","_",$request->param('Action'));
$this->requestParams = $request->requestVars();
if(!$this->action) $this->action = 'index';
$methodName = $this->action;
// run & init are manually disabled, because they create infinite loops and other dodgy situations
if($this->checkAccessAction($this->action) && !in_array(strtolower($this->action), array('run', 'init'))) {
if($this->hasMethod($methodName)) {
$result = $this->$methodName($request);
// Method returns an array, that is used to customise the object before rendering with a template
if(is_array($result)) {
return $this->getViewer($this->action)->process($this->customise($result));
// Method returns a string / object, in which case we just return that
} else {
return $result;
}
// There is no method, in which case we just render this object using a (possibly alternate) template
} else {
return $this->getViewer($this->action)->process($this);
}
} else {
return $this->httpError(403, "Action '$this->action' isn't allowed on class $this->class");
}
}
function setURLParams($urlParams) {
$this->urlParams = $urlParams;
}
/**
* @return array The parameters extracted from the URL by the {@link Director}.
*/
function getURLParams() {
return $this->urlParams;
}
/**
* Returns the HTTPResponse object that this controller is building up.
* Can be used to set the status code and headers
*/
function getResponse() {
return $this->response;
}
/**
* Get the request with which this controller was called (if any).
* Usually set in {@link handleRequest()}.
*
* @return HTTPRequest
*/
function getRequest() {
return $this->request;
}
protected $baseInitCalled = false;
/**
* Executes this controller, and return an {@link HTTPResponse} object with the result.
*
* This method first does a few set-up activities:
* - Push this controller ont to the controller stack - see {@link Controller::curr()} for information about this.
* - Call {@link init()}
*
* Then it looks for the action method. The action is taken from $this->urlParams['Action'] - for this reason, it's important
* to have $Action included in your Director rule
*
* If $requestParams['executeForm'] is set, then the Controller assumes that we're processing a form. This is usually
* set by adding ?executeForm=XXX to the form's action URL. Form processing differs in the following ways:
* - The action name will be the name of the button clicked. If no button-click can be detected, the first button in the
* list will be assumed.
* - If the given action method doesn't exist on the controller, Controller will look for that method on the Form object.
* this lets developers package both a form and its action handlers in a single subclass of Form.
*
* NOTE: You should rarely need to overload run() - this kind of change is only really appropriate for things like nested
* controllers - {@link ModelAsController} and {@link RootURLController} are two examples here. If you want to make more
* orthodox functionality, it's better to overload {@link init()} or {@link index()}.
*
* Execute the appropriate action handler. If none is given, use defaultAction to display
* a template. The default action will be appropriate in most cases where displaying data
* is the core goal; the Viewer can call methods on the controller to get the data it needs.
*
* @param array $requestParams GET and POST variables.
* @return HTTPResponse The response that this controller produces, including HTTP headers such as redirection info
*/
/**
* Return the object that is going to own a form that's being processed, and handle its execution.
* Note that the result needn't be an actual controller object.
*/
function getFormOwner() {
// Get the appropraite ocntroller: sometimes we want to get a form from another controller
if(isset($this->requestParams['formController'])) {
$formController = Director::getControllerForURL($this->requestParams['formController']);
while(is_a($formController, 'NestedController')) {
$formController = $formController->getNestedController();
}
return $formController;
} else {
return $this;
}
}
/**
* This is the default action handler used if a method doesn't exist.
* It will process the controller object with the template returned by {@link getViewer()}
*/
function defaultAction($action) {
return $this->getViewer($action)->process($this);
}
/**
* Returns the action that is being executed on this controller.
*/
function getAction() {
return $this->action;
}
/**
* Return an SSViewer object to process the data
* @return SSViewer The viewer identified being the default handler for this Controller/Action combination
*/
function getViewer($action) {
// Hard-coded templates
if($this->templates[$action]) {
$templates = $this->templates[$action];
} else if($this->templates['index']) {
$templates = $this->templates['index'];
} else if($this->template) {
$templates = $this->template;
} else {
$parentClass = $this->class;
if($action && $action != 'index') {
$parentClass = $this->class;
while($parentClass != "Controller") {
$templates[] = strtok($parentClass,'_') . '_' . $action;
$parentClass = get_parent_class($parentClass);
}
}
$parentClass = $this->class;
while($parentClass != "Controller") {
$templates[] = strtok($parentClass,'_');
$parentClass = get_parent_class($parentClass);
}
$templates = array_unique($templates);
}
return new SSViewer($templates);
}
/**
* Render the current controller with the templates determined
* by {@link getViewer()}.
*
* @param array $params Key-value array for custom template variables (Optional)
* @return string Parsed template content
*/
function render($params = null) {
$template = $this->getViewer($this->getAction());
// if the object is already customised (e.g. through Controller->run()), use it
$obj = ($this->customisedObj) ? $this->customisedObj : $this;
if($params) $obj = $this->customise($params);
return $template->process($obj);
}
/**
* Call this to disable basic authentication on test sites.
* must be called in the init() method
* @deprecated Use BasicAuth::disable() instead? This is used in CliController - it should be updated.
*/
function disableBasicAuth() {
$this->basicAuthEnabled = false;
}
/**
* Initialisation function that is run before any action on the controller is called.
*
* @uses BasicAuth::requireLogin()
*/
function init() {
// Test and development sites should be secured, via basic-auth
if(ClassInfo::hasTable("Group") && ClassInfo::hasTable("Member") && Director::isTest() && $this->basicAuthEnabled) {
BasicAuth::requireLogin("SilverStripe test website. Use your CMS login", "ADMIN");
}
//
Cookie::set("PastVisitor", true);
// ClassInfo::hasTable() called to ensure that we're not in a very-first-setup stage
if(ClassInfo::hasTable("Group") && ClassInfo::hasTable("Member") && ($member = Member::currentUser())) {
Cookie::set("PastMember", true);
DB::query("UPDATE Member SET LastVisited = NOW() WHERE ID = $member->ID", null);
}
// This is used to test that subordinate controllers are actually calling parent::init() - a common bug
$this->baseInitCalled = true;
}
/**
* @deprecated use Controller::curr() instead
* @returns Controller
*/
public static function currentController() {
user_error('Controller::currentController() is deprecated. Use Controller::curr() instead.', E_USER_NOTICE);
return self::curr();
}
/**
* Returns the current controller
* @returns Controller
*/
public static function curr() {
if(Controller::$controller_stack) {
return Controller::$controller_stack[0];
} else {
user_error("No current controller available", E_USER_WARNING);
}
}
/**
* Tests whether we have a currently active controller or not
* @return boolean True if there is at least 1 controller in the stack.
*/
public static function has_curr() {
return Controller::$controller_stack ? true : false;
}
/**
* Returns true if the member is allowed to do the given action.
* @param perm The permission to be checked, such as 'View'.
* @param member The member whose permissions need checking. Defaults to the currently logged
* in user.
* @return boolean
* @deprecated I don't believe that the system has widespread use/support of this.
*/
function can($perm, $member = null) {
if(!$member) $member = Member::currentUser();
if($this->hasMethod($methodName = 'can' . $perm)) {
return $this->$methodName($member);
} else {
return true;
}
}
//-----------------------------------------------------------------------------------
/**
* returns a date object for use within a template
* Usage: $Now.Year - Returns 2006
* @return Date The current date
*/
function Now() {
$d = new Date(null);
$d->setVal(date("Y-m-d h:i:s"));
return $d;
}
/**
* Returns a link to any other page
* @deprecated It's unclear what value this has; construct a link manually or use your own custom link-gen functions.
*/
function LinkTo($a, $b) {
return Director::baseURL() . $a . '/' . $b;
}
/**
* Returns an absolute link to this controller
*/
function AbsoluteLink() {
return Director::absoluteURL($this->Link());
}
/**
* Returns the currently logged in user
*/
function CurrentMember() {
return Member::currentUser();
}
/**
* Returns true if the visitor has been here before
* @return boolean
*/
function PastVisitor() {
return Cookie::get("PastVisitor") ? true : false;
}
/**
* Return true if the visitor has signed up for a login account before
* @return boolean
*/
function PastMember() {
return Cookie::get("PastMember") ? true : false;
}
/**
* Pushes this controller onto the stack of current controllers.
* This means that any redirection, session setting, or other things that rely on Controller::curr() will now write to this
* controller object.
*/
function pushCurrent() {
array_unshift(self::$controller_stack, $this);
// Create a new session object
if(!$this->session) {
if(isset(self::$controller_stack[1])) {
$this->session = self::$controller_stack[1]->getSession();
} else {
$this->session = new Session(null);
}
}
}
/**
* Pop this controller off the top of the stack.
*/
function popCurrent() {
if($this === self::$controller_stack[0]) {
array_shift(self::$controller_stack);
} else {
user_error("popCurrent called on $this->class controller, but it wasn't at the top of the stack", E_USER_WARNING);
}
}
/**
* Redirct to the given URL.
* It is generally recommended to call Director::redirect() rather than calling this function directly.
*/
function redirect($url, $code=302) {
if($this->response->getHeader('Location')) {
user_error("Already directed to " . $this->response->getHeader('Location') . "; now trying to direct to $url", E_USER_WARNING);
return;
}
// Attach site-root to relative links, if they have a slash in them
if($url == "" || $url[0] == '?' || (substr($url,0,4) != "http" && $url[0] != "/" && strpos($url,'/') !== false)){
$url = Director::baseURL() . $url;
}
$this->response->redirect($url, $code);
}
/**
* Tests whether a redirection has been requested.
* @return string If redirect() has been called, it will return the URL redirected to. Otherwise, it will return null;
*/
function redirectedTo() {
return $this->response->getHeader('Location');
}
/**
* Get the Session object representing this Controller's session
* @return Session
*/
function getSession() {
return $this->session;
}
/**
* Set the Session object.
*/
function setSession(Session $session) {
$this->session = $session;
}
/**
* Returns true if this controller is processing an ajax request
* @return boolean True if this controller is processing an ajax request
*/
function isAjax() {
return (
isset($this->requestParams['ajax']) ||
(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest")
);
}
/**
* Joins two link segments together, putting a slash between them if necessary.
* Use this for building the results of Link() methods.
*
* If either of the links have query strings, then they will be combined and put at the end of the resulting url.
*/
static function join_links() {
$args = func_get_args();
$result = "";
$querystrings = array();
foreach($args as $arg) {
if(strpos($arg,'?') !== false) {
list($arg, $suffix) = explode('?',$arg,2);
$querystrings[] = $suffix;
}
if($arg) {
if($result && substr($result,-1) != '/' && $arg[0] != '/') $result .= "/$arg";
else $result .= $arg;
}
}
if($querystrings) $result .= '?' . implode('&', $querystrings);
return $result;
}
}
?>