mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60205 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
9ac464cc57
commit
03fcc80e19
20
_config.php
20
_config.php
@ -17,6 +17,26 @@
|
|||||||
* @subpackage core
|
* @subpackage core
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Default director
|
||||||
|
Director::addRules(10, array(
|
||||||
|
'Security' => 'Security',
|
||||||
|
//'Security/$Action/$ID' => 'Security',
|
||||||
|
'db/$Action' => 'DatabaseAdmin',
|
||||||
|
'$Controller' => array(
|
||||||
|
),
|
||||||
|
'images/$Action/$Class/$ID/$Field' => 'Image_Uploader',
|
||||||
|
'' => 'RootURLController',
|
||||||
|
'sitemap.xml' => 'GoogleSitemap',
|
||||||
|
'api/v1' => 'RestfulServer',
|
||||||
|
));
|
||||||
|
|
||||||
|
Director::addRules(1, array(
|
||||||
|
'$URLSegment/$Action/$ID/$OtherID' => array(
|
||||||
|
'_PopTokeniser' => 1,
|
||||||
|
'Controller' => 'ModelAsController',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PHP 5.2 has a namespace conflict with our datetime class,
|
* PHP 5.2 has a namespace conflict with our datetime class,
|
||||||
* for legacy support, we use this overload method.
|
* for legacy support, we use this overload method.
|
||||||
|
@ -28,8 +28,21 @@
|
|||||||
* @subpackage api
|
* @subpackage api
|
||||||
*/
|
*/
|
||||||
class RestfulServer extends Controller {
|
class RestfulServer extends Controller {
|
||||||
|
static $url_handlers = array(
|
||||||
|
'$ClassName/#ID' => 'handleItem',
|
||||||
|
'$ClassName' => 'handleList',
|
||||||
|
);
|
||||||
|
|
||||||
protected static $api_base = "api/v1/";
|
protected static $api_base = "api/v1/";
|
||||||
|
|
||||||
|
function handleItem($params) {
|
||||||
|
return new RestfulServer_Item(DataObject::get_by_id($params["ClassName"], $params["ID"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleList($params) {
|
||||||
|
return new RestfulServer_List(DataObject::get($params["ClassName"],""));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handler acts as the switchboard for the controller.
|
* This handler acts as the switchboard for the controller.
|
||||||
* Since no $Action url-param is set, all requests are sent here.
|
* Since no $Action url-param is set, all requests are sent here.
|
||||||
@ -319,3 +332,42 @@ class RestfulServer extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restful server handler for a DataObjectSet
|
||||||
|
*/
|
||||||
|
class RestfulServer_List {
|
||||||
|
static $url_handlers = array(
|
||||||
|
'#ID' => 'handleItem',
|
||||||
|
);
|
||||||
|
|
||||||
|
function __construct($list) {
|
||||||
|
$this->list = $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItem($params) {
|
||||||
|
return new RestulServer_Item($this->list->getById($params['ID']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restful server handler for a single DataObject
|
||||||
|
*/
|
||||||
|
class RestfulServer_Item {
|
||||||
|
static $url_handlers = array(
|
||||||
|
'$Relation' => 'handleRelation',
|
||||||
|
);
|
||||||
|
|
||||||
|
function __construct($item) {
|
||||||
|
$this->item = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRelation($params) {
|
||||||
|
$funcName = $params['Relation'];
|
||||||
|
$relation = $this->item->$funcName();
|
||||||
|
|
||||||
|
if($relation instanceof DataObjectSet) return new RestfulServer_List($relation);
|
||||||
|
else return new RestfulServer_Item($relation)l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -865,7 +865,6 @@ class ViewableData extends Object implements IteratorAggregate {
|
|||||||
return Convert::raw2att(implode(" ", $classes));
|
return Convert::raw2att(implode(" ", $classes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object-casting information for class methods
|
* Object-casting information for class methods
|
||||||
* @var mixed
|
* @var mixed
|
||||||
|
@ -28,7 +28,6 @@ class ContentController extends Controller {
|
|||||||
public function __construct($dataRecord) {
|
public function __construct($dataRecord) {
|
||||||
$this->dataRecord = $dataRecord;
|
$this->dataRecord = $dataRecord;
|
||||||
$this->failover = $this->dataRecord;
|
$this->failover = $this->dataRecord;
|
||||||
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,27 +9,12 @@
|
|||||||
* @package sapphire
|
* @package sapphire
|
||||||
* @subpackage control
|
* @subpackage control
|
||||||
*/
|
*/
|
||||||
class Controller extends ViewableData {
|
class Controller extends RequestHandlingData {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a list of actions that are allowed to be called on this controller.
|
* An array of arguments extracted from the URL
|
||||||
* The variable should be an array of action names. This sample shows the different values that it can contain:
|
|
||||||
*
|
|
||||||
* <code>
|
|
||||||
* array(
|
|
||||||
* 'someaction', // someaction can be accessed by anyone, any time
|
|
||||||
* 'otheraction' => true, // So can otheraction
|
|
||||||
* 'restrictedaction' => 'ADMIN', // restrictedaction can only be people with ADMIN privilege
|
|
||||||
* 'complexaction' '->canComplexAction' // complexaction can only be accessed if $this->canComplexAction() returns true
|
|
||||||
* );
|
|
||||||
* </code>
|
|
||||||
*/
|
*/
|
||||||
static $allowed_actions = null;
|
|
||||||
|
|
||||||
protected $urlParams;
|
protected $urlParams;
|
||||||
|
|
||||||
protected $requestParams;
|
protected $requestParams;
|
||||||
|
|
||||||
protected $action;
|
protected $action;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +35,90 @@ class Controller extends ViewableData {
|
|||||||
*/
|
*/
|
||||||
protected $response;
|
protected $response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default URL handlers - (Action)/(ID)/(OtherID)
|
||||||
|
*/
|
||||||
|
static $url_handlers = array(
|
||||||
|
'$Action/$ID/$OtherID' => 'handleAction',
|
||||||
|
);
|
||||||
|
|
||||||
|
static $allowed_actions = array(
|
||||||
|
'handleAction',
|
||||||
|
'handleIndex',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles HTTP requests.
|
||||||
|
* @param $request The {@link HTTPRequest} object that is responsible for distributing request parsing.
|
||||||
|
*/
|
||||||
|
function handleRequest($request) {
|
||||||
|
$this->pushCurrent();
|
||||||
|
$this->urlParams = $request->allParams();
|
||||||
|
$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) {
|
||||||
|
$this->response = $body;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if(is_object($body)) $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
|
||||||
|
$this->urlParams = array_merge($this->urlParams, $request->latestParams());
|
||||||
|
$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) {
|
function setURLParams($urlParams) {
|
||||||
$this->urlParams = $urlParams;
|
$this->urlParams = $urlParams;
|
||||||
}
|
}
|
||||||
@ -99,185 +168,6 @@ class Controller extends ViewableData {
|
|||||||
* @param array $requestParams GET and POST variables.
|
* @param array $requestParams GET and POST variables.
|
||||||
* @return HTTPResponse The response that this controller produces, including HTTP headers such as redirection info
|
* @return HTTPResponse The response that this controller produces, including HTTP headers such as redirection info
|
||||||
*/
|
*/
|
||||||
function run($requestParams) {
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("Controller", "run");
|
|
||||||
$this->pushCurrent();
|
|
||||||
|
|
||||||
$this->response = new HTTPResponse();
|
|
||||||
$this->requestParams = $requestParams;
|
|
||||||
|
|
||||||
$this->action = isset($this->urlParams['Action']) ? str_replace("-","_",$this->urlParams['Action']) : "";
|
|
||||||
if(!$this->action) $this->action = 'index';
|
|
||||||
|
|
||||||
// Check security on the controller
|
|
||||||
// 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'))) {
|
|
||||||
user_error("Disallowed action: '$this->action' on controller '$this->class'", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look at the action variables for forms
|
|
||||||
$funcName = null;
|
|
||||||
foreach($this->requestParams as $paramName => $paramVal) {
|
|
||||||
if(substr($paramName,0,7) == 'action_') {
|
|
||||||
// Cleanup action_, _x and _y from image fields
|
|
||||||
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form handler
|
|
||||||
if(isset($this->requestParams['executeForm']) && is_string($this->requestParams['executeForm'])) {
|
|
||||||
if(isset($funcName)) {
|
|
||||||
Form::set_current_action($funcName);
|
|
||||||
}
|
|
||||||
|
|
||||||
$formOwner = $this->getFormOwner();
|
|
||||||
|
|
||||||
// Create the form object
|
|
||||||
$form = $formOwner;
|
|
||||||
|
|
||||||
$formObjParts = explode('.', $this->requestParams['executeForm']);
|
|
||||||
foreach($formObjParts as $formMethod){
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("Calling $formMethod", "on $form->class");
|
|
||||||
$form = $form->$formMethod();
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Calling $formMethod", "on $form->class");
|
|
||||||
if(!$form) break; //user_error("Form method '" . $this->requestParams['executeForm'] . "' returns null in controller class '$this->class' ($_SERVER[REQUEST_URI])", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Populate the form
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("Controller", "populate form");
|
|
||||||
if($form){
|
|
||||||
$form->loadDataFrom($this->requestParams, true);
|
|
||||||
// disregard validation if a single field is called
|
|
||||||
|
|
||||||
|
|
||||||
if(!isset($_REQUEST['action_callfieldmethod'])) {
|
|
||||||
$valid = $form->beforeProcessing();
|
|
||||||
if(!$valid) {
|
|
||||||
$this->popCurrent();
|
|
||||||
return $this->response;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
$fieldcaller = $form->dataFieldByName($requestParams['fieldName']);
|
|
||||||
if(is_a($fieldcaller, "TableListField")){
|
|
||||||
if($fieldcaller->hasMethod('php')){
|
|
||||||
$valid = $fieldcaller->php($requestParams);
|
|
||||||
if(!$valid) exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the action wasnt' set, choose the default on the form.
|
|
||||||
if(!isset($funcName) && $defaultAction = $form->defaultAction()){
|
|
||||||
$funcName = $defaultAction->actionName();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($funcName)) {
|
|
||||||
$form->setButtonClicked($funcName);
|
|
||||||
}
|
|
||||||
|
|
||||||
}else{
|
|
||||||
user_error("No form (" . Session::get('CMSMain.currentPage') . ") returned by $formOwner->class->$_REQUEST[executeForm]", E_USER_WARNING);
|
|
||||||
}
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Controller", "populate form");
|
|
||||||
|
|
||||||
if(!isset($funcName)) {
|
|
||||||
user_error("No action button has been clicked in this form executon, and no default has been allowed", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protection against CSRF attacks
|
|
||||||
if($form->securityTokenEnabled()) {
|
|
||||||
$securityID = Session::get('SecurityID');
|
|
||||||
|
|
||||||
if(!$securityID || !isset($this->requestParams['SecurityID']) || $securityID != $this->requestParams['SecurityID']) {
|
|
||||||
// Don't show error on live sites, as spammers create a million of these
|
|
||||||
if(!Director::isLive()) {
|
|
||||||
trigger_error("Security ID doesn't match, possible CRSF attack.", E_USER_ERROR);
|
|
||||||
} else {
|
|
||||||
die();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// First, try a handler method on the controller
|
|
||||||
if($this->hasMethod($funcName) || !$form) {
|
|
||||||
if(isset($_GET['debug_controller'])){
|
|
||||||
Debug::show("Found function $funcName on the controller");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("$this->class::$funcName (controller action)");
|
|
||||||
$result = $this->$funcName($this->requestParams, $form);
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("$this->class::$funcName (controller action)");
|
|
||||||
|
|
||||||
} else if(isset($formOwner) && $formOwner->hasMethod($funcName)) {
|
|
||||||
$result = $formOwner->$funcName($this->requestParams, $form);
|
|
||||||
|
|
||||||
// Otherwise, try a handler method on the form object
|
|
||||||
} else {
|
|
||||||
if(isset($_GET['debug_controller'])) {
|
|
||||||
Debug::show("Found function $funcName on the form object");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("$form->class::$funcName (form action)");
|
|
||||||
$result = $form->$funcName($this->requestParams, $form);
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("$form->class::$funcName (form action)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal action
|
|
||||||
} else {
|
|
||||||
if(!isset($funcName)) $funcName = $this->action;
|
|
||||||
|
|
||||||
if($this->hasMethod($funcName)) {
|
|
||||||
if(isset($_GET['debug_controller'])) Debug::show("Found function $funcName on the $this->class controller");
|
|
||||||
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("$this->class::$funcName (controller action)");
|
|
||||||
|
|
||||||
$result = $this->$funcName($this->urlParams);
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("$this->class::$funcName (controller action)");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if(isset($_GET['debug_controller'])) Debug::show("Running default action for $funcName on the $this->class controller" );
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("Controller::defaultAction($funcName)");
|
|
||||||
$result = $this->defaultAction($funcName, $this->urlParams);
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Controller::defaultAction($funcName)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If your controller function returns an array, then add that data to the
|
|
||||||
// default template
|
|
||||||
|
|
||||||
if(is_array($result)) {
|
|
||||||
$extended = $this->customise($result);
|
|
||||||
$viewer = $this->getViewer($funcName);
|
|
||||||
|
|
||||||
$result = $viewer->process($extended);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->response->setBody($result);
|
|
||||||
|
|
||||||
if($result) ContentNegotiator::process($this->response);
|
|
||||||
|
|
||||||
// Set up HTTP cache headers
|
|
||||||
HTTP::add_cache_headers($this->response);
|
|
||||||
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Controller", "run");
|
|
||||||
|
|
||||||
$this->popCurrent();
|
|
||||||
return $this->response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the object that is going to own a form that's being processed, and handle its execution.
|
* Return the object that is going to own a form that's being processed, and handle its execution.
|
||||||
@ -552,56 +442,6 @@ class Controller extends ViewableData {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that the given action is allowed to be called on this controller.
|
|
||||||
* This method is called by run() and makes use of {@link self::$allowed_actions}.
|
|
||||||
*/
|
|
||||||
function checkAccessAction($action) {
|
|
||||||
$action = strtolower($action);
|
|
||||||
|
|
||||||
// Collate self::$allowed_actions from this class and all parent classes
|
|
||||||
$access = null;
|
|
||||||
$className = $this->class;
|
|
||||||
while($className != 'Controller') {
|
|
||||||
// Merge any non-null parts onto $access.
|
|
||||||
$accessPart = eval("return $className::\$allowed_actions;");
|
|
||||||
if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
|
|
||||||
|
|
||||||
// Build an array of parts for checking if part[0] == part[1], which means that this class doesn't directly define it.
|
|
||||||
$accessParts[] = $accessPart;
|
|
||||||
|
|
||||||
$className = get_parent_class($className);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add $allowed_actions from extensions
|
|
||||||
if($this->extension_instances) {
|
|
||||||
foreach($this->extension_instances as $inst) {
|
|
||||||
$accessPart = $inst->stat('allowed_actions');
|
|
||||||
if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($access === null || (isset($accessParts[1]) && $accessParts[0] === $accessParts[1])) {
|
|
||||||
// user_error("Deprecated: please define static \$allowed_actions on your Controllers for security purposes", E_USER_NOTICE);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($action == 'index') return true;
|
|
||||||
|
|
||||||
if(isset($access[$action])) {
|
|
||||||
$test = $access[$action];
|
|
||||||
if($test === true) return true;
|
|
||||||
if(substr($test,0,2) == '->') {
|
|
||||||
$funcName = substr($test,2);
|
|
||||||
return $this->$funcName();
|
|
||||||
}
|
|
||||||
if(Permission::check($test)) return true;
|
|
||||||
} else if((($key = array_search($action, $access)) !== false) && is_numeric($key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
@ -71,41 +71,51 @@ class Director {
|
|||||||
/**
|
/**
|
||||||
* Process the given URL, creating the appropriate controller and executing it.
|
* Process the given URL, creating the appropriate controller and executing it.
|
||||||
*
|
*
|
||||||
* This method will:
|
* Request processing is handled as folows:
|
||||||
* - iterate over all of the rules given in {@link Director::addRules()}, and find the first one that matches.
|
* - Director::direct() creates a new HTTPResponse object and passes this to Director::handleRequest().
|
||||||
* - instantiate the {@link Controller} object required by that rule, and call {@link Controller::setURLParams()} to give the URL paramters to the controller.
|
* - Director::handleRequest($request) checks each of the Director rules and identifies a controller to handle this
|
||||||
* - link the Controller's session to PHP's main session, using {@link Controller::setSession()}.
|
* request.
|
||||||
* - call {@link Controller::run()} on that controller
|
* - Controller::handleRequest($request) is then called. This will find a rule to handle the URL, and call the rule
|
||||||
* - save the Controller's session back into PHP's main session.
|
* handling method.
|
||||||
* - output the response to the browser, using {@link HTTPResponse::output()}.
|
* - RequestHandlingData::handleRequest($request) is recursively called whenever a rule handling method returns a
|
||||||
|
* RequestHandlingData object.
|
||||||
|
*
|
||||||
|
* In addition to request processing, Director will manage the session, and perform the output of the actual response
|
||||||
|
* to the browser.
|
||||||
*
|
*
|
||||||
* @param $url String, the URL the user is visiting, without the querystring.
|
* @param $url String, the URL the user is visiting, without the querystring.
|
||||||
* @uses getControllerForURL() rule-lookup logic is handled by this.
|
* @uses handleRequest() rule-lookup logic is handled by this.
|
||||||
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
|
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
|
||||||
*/
|
*/
|
||||||
function direct($url) {
|
function direct($url) {
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("Director","direct");
|
$req = new HTTPRequest($_SERVER['REQUEST_METHOD'], $url, $_GET, array_merge((array)$_POST, (array)$_FILES));
|
||||||
$controllerObj = Director::getControllerForURL($url);
|
|
||||||
|
|
||||||
if(is_string($controllerObj) && substr($controllerObj,0,9) == 'redirect:') {
|
// Load the session into the controller
|
||||||
|
$session = new Session($_SESSION);
|
||||||
|
$result = Director::handleRequest($req, $session);
|
||||||
|
$session->inst_save();
|
||||||
|
|
||||||
|
// Return code for a redirection request
|
||||||
|
if(is_string($result) && substr($result,0,9) == 'redirect:') {
|
||||||
$response = new HTTPResponse();
|
$response = new HTTPResponse();
|
||||||
$response->redirect(substr($controllerObj, 9));
|
$response->redirect(substr($result, 9));
|
||||||
$response->output();
|
$response->output();
|
||||||
} else if($controllerObj) {
|
|
||||||
// Load the session into the controller
|
|
||||||
$controllerObj->setSession(new Session($_SESSION));
|
|
||||||
|
|
||||||
$response = $controllerObj->run(array_merge((array)$_GET, (array)$_POST, (array)$_FILES));
|
// Handle a controller
|
||||||
|
} else if($result) {
|
||||||
|
if($result instanceof HTTPResponse) {
|
||||||
|
$response = $result;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$response = new HTTPResponse();
|
||||||
|
$response->setBody($result);
|
||||||
|
}
|
||||||
|
|
||||||
$controllerObj->getSession()->inst_save();
|
|
||||||
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("Outputting to browser");
|
|
||||||
$response->output();
|
$response->output();
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Outputting to browser");
|
|
||||||
|
//$controllerObj->getSession()->inst_save();
|
||||||
|
|
||||||
}
|
}
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Director","direct");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,117 +124,79 @@ class Director {
|
|||||||
* This method is the counterpart of Director::direct() that is used in functional testing. It will execute the URL given,
|
* This method is the counterpart of Director::direct() that is used in functional testing. It will execute the URL given,
|
||||||
*
|
*
|
||||||
* @param $url The URL to visit
|
* @param $url The URL to visit
|
||||||
* @param $post The $_POST & $_FILES variables
|
* @param $postVars The $_POST & $_FILES variables
|
||||||
* @param $session The {@link Session} object representing the current session. By passing the same object to multiple
|
* @param $session The {@link Session} object representing the current session. By passing the same object to multiple
|
||||||
* calls of Director::test(), you can simulate a peristed session.
|
* calls of Director::test(), you can simulate a peristed session.
|
||||||
|
* @param $httpMethod The HTTP method, such as GET or POST. It will default to POST if postVars is set, GET otherwise
|
||||||
*
|
*
|
||||||
* @uses getControllerForURL() The rule-lookup logic is handled by this.
|
* @uses getControllerForURL() The rule-lookup logic is handled by this.
|
||||||
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
|
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
|
||||||
*/
|
*/
|
||||||
function test($url, $post = null, $session = null) {
|
function test($url, $postVars = null, $session = null, $httpMethod = null) {
|
||||||
|
if(!$httpMethod) $httpMethod = $postVars ? "POST" : "GET";
|
||||||
|
|
||||||
$getVars = array();
|
$getVars = array();
|
||||||
if(strpos($url,'?') !== false) {
|
if(strpos($url,'?') !== false) {
|
||||||
list($url, $getVarsEncoded) = explode('?', $url, 2);
|
list($url, $getVarsEncoded) = explode('?', $url, 2);
|
||||||
parse_str($getVarsEncoded, $getVars);
|
parse_str($getVarsEncoded, $getVars);
|
||||||
}
|
}
|
||||||
|
|
||||||
$existingRequestVars = $_REQUEST;
|
if(!$session) $session = new Session(null);
|
||||||
$existingGetVars = $_GET;
|
|
||||||
$existingPostVars = $_POST;
|
|
||||||
$existingSessionVars = $_SESSION;
|
|
||||||
|
|
||||||
$_REQUEST = $existingRequestVars;
|
$req = new HTTPRequest($httpMethod, $url, $getVars, $postVars);
|
||||||
$_GET = $existingGetVars;
|
$result = Director::handleRequest($req, $session);
|
||||||
$_POST = $existingPostVars;
|
|
||||||
$_SESSION = $existingSessionVars;
|
|
||||||
|
|
||||||
$_REQUEST = array_merge((array)$getVars, (array)$post);
|
return $result;
|
||||||
$_GET = (array)$getVars;
|
|
||||||
$_POST = (array)$post;
|
|
||||||
$_SESSION = $session ? $session->inst_getAll() : array();
|
|
||||||
|
|
||||||
$controllerObj = Director::getControllerForURL($url);
|
|
||||||
|
|
||||||
// Load the session into the controller
|
|
||||||
$controllerObj->setSession($session ? $session : new Session(null));
|
|
||||||
|
|
||||||
if(is_string($controllerObj) && substr($controllerObj,0,9) == 'redirect:') {
|
|
||||||
user_error("Redirection not implemented in Director::test", E_USER_ERROR);
|
|
||||||
|
|
||||||
} else if($controllerObj) {
|
|
||||||
$response = $controllerObj->run( array_merge($getVars, (array)$post) );
|
|
||||||
$_REQUEST = $existingRequestVars;
|
|
||||||
$_GET = $existingGetVars;
|
|
||||||
$_POST = $existingPostVars;
|
|
||||||
$_SESSION = $existingSessionVars;
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the controller that should be used to handle the given URL.
|
* Handle an HTTP request, defined with a HTTPRequest object.
|
||||||
* @todo More information about director rules.
|
|
||||||
*/
|
*/
|
||||||
static function getControllerForURL($url) {
|
protected static function handleRequest(HTTPRequest $request, Session $session) {
|
||||||
if(isset($_GET['debug_profile'])) Profiler::mark("Director","getControllerForURL");
|
|
||||||
$url = preg_replace( array( '/\/+/','/^\//', '/\/$/'),array('/','',''),$url);
|
|
||||||
$urlParts = split('/+', $url);
|
|
||||||
|
|
||||||
krsort(Director::$rules);
|
krsort(Director::$rules);
|
||||||
|
|
||||||
if(isset($_REQUEST['debug'])) Debug::show(Director::$rules);
|
if(isset($_REQUEST['debug'])) Debug::show(Director::$rules);
|
||||||
|
|
||||||
foreach(Director::$rules as $priority => $rules) {
|
foreach(Director::$rules as $priority => $rules) {
|
||||||
foreach($rules as $pattern => $controller) {
|
foreach($rules as $pattern => $controllerOptions) {
|
||||||
$patternParts = explode('/', $pattern);
|
if(is_string($controllerOptions)) {
|
||||||
$matched = true;
|
if(substr($controllerOptions,0,2) == '->') $controllerOptions = array('Redirect' => substr($controllerOptions,2));
|
||||||
$arguments = array();
|
else $controllerOptions = array('Controller' => $controllerOptions);
|
||||||
foreach($patternParts as $i => $part) {
|
|
||||||
$part = trim($part);
|
|
||||||
if(isset($part[0]) && $part[0] == '$') {
|
|
||||||
$arguments[substr($part,1)] = isset($urlParts[$i]) ? $urlParts[$i] : null;
|
|
||||||
if($part == '$Controller' && !class_exists($arguments['Controller'])) {
|
|
||||||
$matched = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if(!isset($urlParts[$i]) || $urlParts[$i] != $part) {
|
|
||||||
$matched = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if($matched) {
|
|
||||||
|
|
||||||
if(substr($controller,0,2) == '->') {
|
if(($arguments = $request->match($pattern, true)) !== false) {
|
||||||
if(isset($_REQUEST['debug']) && $_REQUEST['debug'] == 1) Debug::message("Redirecting to $controller");
|
// controllerOptions provide some default arguments
|
||||||
|
$arguments = array_merge($controllerOptions, $arguments);
|
||||||
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Director","getControllerForURL");
|
// Find the controller name
|
||||||
|
if(isset($arguments['Controller'])) $controller = $arguments['Controller'];
|
||||||
|
|
||||||
return "redirect:" . Director::absoluteURL(substr($controller,2), true);
|
// Pop additional tokens from the tokeniser if necessary
|
||||||
|
if(isset($controllerOptions['_PopTokeniser'])) {
|
||||||
|
$request->shift($controllerOptions['_PopTokeniser']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle redirections
|
||||||
|
if(isset($arguments['Redirect'])) {
|
||||||
|
return "redirect:" . Director::absoluteURL($arguments['Redirect'], true);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if(isset($arguments['Controller']) && $controller == "*") {
|
/*
|
||||||
$controller = $arguments['Controller'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($_REQUEST['debug'])) Debug::message("Using controller $controller");
|
|
||||||
if(isset($arguments['Action'])) {
|
if(isset($arguments['Action'])) {
|
||||||
$arguments['Action'] = str_replace('-','',$arguments['Action']);
|
$arguments['Action'] = str_replace('-','',$arguments['Action']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($arguments['Action']) && ClassInfo::exists($controller.'_'.$arguments['Action']))
|
if(isset($arguments['Action']) && ClassInfo::exists($controller.'_'.$arguments['Action']))
|
||||||
$controller = $controller.'_'.$arguments['Action'];
|
$controller = $controller.'_'.$arguments['Action'];
|
||||||
|
*/
|
||||||
Director::$urlParams = $arguments;
|
|
||||||
$controllerObj = new $controller();
|
|
||||||
|
|
||||||
$controllerObj->setURLParams($arguments);
|
|
||||||
|
|
||||||
if(isset($arguments['URLSegment'])) self::$urlSegment = $arguments['URLSegment'] . "/";
|
if(isset($arguments['URLSegment'])) self::$urlSegment = $arguments['URLSegment'] . "/";
|
||||||
|
|
||||||
if(isset($_GET['debug_profile'])) Profiler::unmark("Director","getControllerForURL");
|
Director::$urlParams = $arguments;
|
||||||
|
|
||||||
return $controllerObj;
|
$controllerObj = new $controller();
|
||||||
|
$controllerObj->setSession($session);
|
||||||
|
|
||||||
|
return $controllerObj->handleRequest($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
225
core/control/HTTPRequest.php
Normal file
225
core/control/HTTPRequest.php
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method (GET/POST/PUT/DELETE).
|
||||||
|
* This is used by {@link RequestHandlingData} objects to decide what to do.
|
||||||
|
*
|
||||||
|
* The intention is that a single HTTPRequest object can be passed from one object to another, each object calling
|
||||||
|
* match() to get the information that they need out of the URL. This is generally handled by
|
||||||
|
* {@link RequestHandlingData::handleRequest()}.
|
||||||
|
*/
|
||||||
|
class HTTPRequest extends Object {
|
||||||
|
/**
|
||||||
|
* The non-extension parts of the URL, separated by "/"
|
||||||
|
*/
|
||||||
|
protected $dirParts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL extension
|
||||||
|
*/
|
||||||
|
protected $extension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP method
|
||||||
|
*/
|
||||||
|
protected $httpMethod;
|
||||||
|
|
||||||
|
protected $getVars = array();
|
||||||
|
protected $postVars = array();
|
||||||
|
|
||||||
|
protected $allParams = array();
|
||||||
|
protected $latestParams = array();
|
||||||
|
|
||||||
|
protected $unshiftedButParsedParts = 0;
|
||||||
|
|
||||||
|
function getVars() {
|
||||||
|
return $this->getVars;
|
||||||
|
}
|
||||||
|
function postVars() {
|
||||||
|
return $this->postVars;
|
||||||
|
}
|
||||||
|
function requestVars() {
|
||||||
|
return array_merge($this->getVars, $this->postVars);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVar($name) {
|
||||||
|
if(isset($this->getVars[$name])) return $this->getVars[$name];
|
||||||
|
}
|
||||||
|
function postVar($name) {
|
||||||
|
if(isset($this->postVars[$name])) return $this->postVars[$name];
|
||||||
|
}
|
||||||
|
function requestVar($name) {
|
||||||
|
if(isset($this->postVars[$name])) return $this->postVars[$name];
|
||||||
|
if(isset($this->getVars[$name])) return $this->getVars[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a HTTPRequest from a URL relative to the site root.
|
||||||
|
*/
|
||||||
|
function __construct($httpMethod, $url, $getVars = array(), $postVars = array()) {
|
||||||
|
$this->httpMethod = $httpMethod;
|
||||||
|
|
||||||
|
$url = preg_replace(array('/\/+/','/^\//', '/\/$/'),array('/','',''), $url);
|
||||||
|
|
||||||
|
if(preg_match('/^(.*)\.([A-Za-z][A-Za-z0-9]*)$/', $url, $matches)) {
|
||||||
|
$url = $matches[1];
|
||||||
|
$this->extension = $matches[2];
|
||||||
|
}
|
||||||
|
if($url) $this->dirParts = split('/+', $url);
|
||||||
|
else $this->dirParts = array();
|
||||||
|
|
||||||
|
$this->getVars = (array)$getVars;
|
||||||
|
$this->postVars = (array)$postVars;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a URL pattern
|
||||||
|
* The pattern can contain a number of segments, separted by / (and an extension indicated by a .)
|
||||||
|
*
|
||||||
|
* The parts can be either literals, or, if they start with a $ they are interpreted as variables.
|
||||||
|
* - Literals must be provided in order to match
|
||||||
|
* - $Variables are optional
|
||||||
|
* - However, if you put ! at the end of a variable, then it becomes mandatory.
|
||||||
|
*
|
||||||
|
* For example:
|
||||||
|
* - admin/crm/list will match admin/crm/$Action/$ID/$OtherID, but it won't match admin/crm/$Action!/$ClassName!
|
||||||
|
*
|
||||||
|
* The pattern can optionally start with an HTTP method and a space. For example, "POST $Controller/$Action".
|
||||||
|
* This is used to define a rule that only matches on a specific HTTP method.
|
||||||
|
*/
|
||||||
|
function match($pattern, $shiftOnSuccess = false) {
|
||||||
|
// Check if a specific method is required
|
||||||
|
if(preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
|
||||||
|
$requiredMethod = $matches[1];
|
||||||
|
if($requiredMethod != $this->httpMethod) return false;
|
||||||
|
|
||||||
|
// If we get this far, we can match the URL pattern as usual.
|
||||||
|
$pattern = $matches[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for the root URL controller
|
||||||
|
if(!$pattern) {
|
||||||
|
return ($this->dirParts == array()) ? array('Matched' => true) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for the '//' marker that represents the "shifting point"
|
||||||
|
$doubleSlashPoint = strpos($pattern, '//');
|
||||||
|
if($doubleSlashPoint !== false) {
|
||||||
|
$shiftCount = substr_count($pattern, '/', 0, $doubleSlashPoint) + 1;
|
||||||
|
$pattern = str_replace('//', '/', $pattern);
|
||||||
|
$patternParts = explode('/', $pattern);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$patternParts = explode('/', $pattern);
|
||||||
|
$shiftCount = sizeof($patternParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
$matched = true;
|
||||||
|
$arguments = array();
|
||||||
|
|
||||||
|
foreach($patternParts as $i => $part) {
|
||||||
|
$part = trim($part);
|
||||||
|
|
||||||
|
// Match a variable
|
||||||
|
if(isset($part[0]) && $part[0] == '$') {
|
||||||
|
// A variable ending in ! is required
|
||||||
|
if(substr($part,-1) == '!') {
|
||||||
|
$varRequired = true;
|
||||||
|
$varName = substr($part,1,-1);
|
||||||
|
} else {
|
||||||
|
$varRequired = false;
|
||||||
|
$varName = substr($part,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if a required variable isn't populated
|
||||||
|
if($varRequired && !isset($this->dirParts[$i])) return false;
|
||||||
|
|
||||||
|
$arguments[$varName] = isset($this->dirParts[$i]) ? $this->dirParts[$i] : null;
|
||||||
|
if($part == '$Controller' && !class_exists($arguments['Controller'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Literal parts must always be there
|
||||||
|
} else if(!isset($this->dirParts[$i]) || $this->dirParts[$i] != $part) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($shiftOnSuccess) {
|
||||||
|
$this->shift($shiftCount);
|
||||||
|
// We keep track of pattern parts that we looked at but didn't shift off.
|
||||||
|
// This lets us say that we have *parsed* the whole URL even when we haven't *shifted* it all
|
||||||
|
$this->unshiftedButParsedParts = sizeof($patternParts) - $shiftCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->latestParams = $arguments;
|
||||||
|
|
||||||
|
// Load the arguments that actually have a value into $this->allParams
|
||||||
|
// This ensures that previous values aren't overridden with blanks
|
||||||
|
foreach($arguments as $k => $v) {
|
||||||
|
if($v) $this->allParams[$k] = $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allParams() {
|
||||||
|
return $this->allParams;
|
||||||
|
}
|
||||||
|
function latestParams() {
|
||||||
|
return $this->latestParams;
|
||||||
|
}
|
||||||
|
function latestParam($name) {
|
||||||
|
if(isset($this->latestParams[$name]))
|
||||||
|
return $this->latestParams[$name];
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function param($name) {
|
||||||
|
if(isset($this->allParams[$name]))
|
||||||
|
return $this->allParams[$name];
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remaining() {
|
||||||
|
return implode("/", $this->dirParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the give pattern is an empty pattern - that is, one that only matches completely parsed
|
||||||
|
* URLs. It will also return true if this is a completely parsed URL and the pattern contains only variable
|
||||||
|
* references.
|
||||||
|
*/
|
||||||
|
function isEmptyPattern($pattern) {
|
||||||
|
if(preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
|
||||||
|
$pattern = $matches[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(trim($pattern) == "") return true;
|
||||||
|
|
||||||
|
if(!$this->dirParts) {
|
||||||
|
return preg_replace('/\$[A-Za-z][A-Za-z0-9]*(\/|$)/','',$pattern) == "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift one or more parts off the beginning of the URL.
|
||||||
|
* If you specify shifting more than 1 item off, then the items will be returned as an array
|
||||||
|
*/
|
||||||
|
function shift($count = 1) {
|
||||||
|
if($count == 1) return array_shift($this->dirParts);
|
||||||
|
else for($i=0;$i<$count;$i++) $return[] = array_shift($this->dirParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the URL has been completely parsed.
|
||||||
|
* This will respect parsed but unshifted directory parts.
|
||||||
|
*/
|
||||||
|
function allParsed() {
|
||||||
|
return sizeof($this->dirParts) <= $this->unshiftedButParsedParts;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -8,13 +8,14 @@
|
|||||||
*/
|
*/
|
||||||
class ModelAsController extends Controller implements NestedController {
|
class ModelAsController extends Controller implements NestedController {
|
||||||
|
|
||||||
public function run($requestParams) {
|
public function handleRequest($request) {
|
||||||
$this->pushCurrent();
|
$this->pushCurrent();
|
||||||
|
$this->urlParams = $request->allParams();
|
||||||
|
|
||||||
$this->init();
|
$this->init();
|
||||||
$nested = $this->getNestedController();
|
$nested = $this->getNestedController();
|
||||||
if(is_object($nested)) {
|
if(is_object($nested)) {
|
||||||
$result = $nested->run($requestParams);
|
$result = $nested->handleRequest($requestParams);
|
||||||
} else {
|
} else {
|
||||||
$result = $nested;
|
$result = $nested;
|
||||||
}
|
}
|
||||||
@ -55,7 +56,6 @@ class ModelAsController extends Controller implements NestedController {
|
|||||||
} else {
|
} else {
|
||||||
$controller = $child;
|
$controller = $child;
|
||||||
}
|
}
|
||||||
$controller->setURLParams($this->urlParams);
|
|
||||||
|
|
||||||
return $controller;
|
return $controller;
|
||||||
} else {
|
} else {
|
||||||
|
169
core/control/RequestHandlingData.php
Normal file
169
core/control/RequestHandlingData.php
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is the base class of any Sapphire object that can be used to handle HTTP requests.
|
||||||
|
*
|
||||||
|
* Any RequestHandlingData object can be made responsible for handling its own segment of the URL namespace.
|
||||||
|
* The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
|
||||||
|
* controller is being used. It will then call handleRequest on that Controller, passing it the parameters that it
|
||||||
|
* parsed from the URL, and the HTTPRequest that contains the remainder of the URL to be parsed.
|
||||||
|
*
|
||||||
|
* In Sapphire, URL parsing is distributed throughout the object graph. For example, suppose that we have a search form
|
||||||
|
* that contains a {@link TreeMultiSelectField}, Groups. We want to use ajax to load segments of this tree as they are needed
|
||||||
|
* rather than downloading the tree right at the beginning. We could use this URL to get the tree segment that appears underneath
|
||||||
|
* Group #36:
|
||||||
|
*
|
||||||
|
* admin/crm/SearchForm/fields/Groups/treesegment/36
|
||||||
|
*
|
||||||
|
* - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
|
||||||
|
* - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and pass control to that.
|
||||||
|
* - Form will determine that fields/Groups is controlled by the Groups field, a TreeMultiselectField, and pass control to that.
|
||||||
|
* - TreeMultiselectField will determine that treesegment/36 is handled by its treesegment() method. This method will return an HTML fragment that is output to the screen.
|
||||||
|
*
|
||||||
|
* {@link RequestHandlingData::handleRequest()} is where this behaviour is implemented.
|
||||||
|
*/
|
||||||
|
class RequestHandlingData extends ViewableData {
|
||||||
|
/**
|
||||||
|
* The default URL handling rules. This specifies that the next component of the URL corresponds to a method to
|
||||||
|
* be called on this RequestHandlingData object.
|
||||||
|
*
|
||||||
|
* The keys of this array are parse rules. See {@link HTTPRequest::match()} for a description of the rules available.
|
||||||
|
*
|
||||||
|
* The values of the array are the method to be called if the rule matches. If this value starts with a '$', then the
|
||||||
|
* named parameter of the parsed URL wil be used to determine the method name.
|
||||||
|
*/
|
||||||
|
static $url_handlers = array(
|
||||||
|
'$Action' => '$Action',
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a list of action handling methods that are allowed to be called directly by URLs.
|
||||||
|
* The variable should be an array of action names. This sample shows the different values that it can contain:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* array(
|
||||||
|
* 'someaction', // someaction can be accessed by anyone, any time
|
||||||
|
* 'otheraction' => true, // So can otheraction
|
||||||
|
* 'restrictedaction' => 'ADMIN', // restrictedaction can only be people with ADMIN privilege
|
||||||
|
* 'complexaction' '->canComplexAction' // complexaction can only be accessed if $this->canComplexAction() returns true
|
||||||
|
* );
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
static $allowed_actions = null;
|
||||||
|
/**
|
||||||
|
* Handles URL requests.
|
||||||
|
*
|
||||||
|
* - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
|
||||||
|
* - If the rule matches, the named method will be called.
|
||||||
|
* - If there is still more URL to be processed, then handleRequest() is called on the object that that method returns.
|
||||||
|
*
|
||||||
|
* Once all of the URL has been processed, the final result is returned. However, if the final result is an array, this
|
||||||
|
* array is interpreted as being additional template data to customise the 2nd to last result with, rather than an object
|
||||||
|
* in its own right. This is most frequently used when a Controller's action will return an array of data with which to
|
||||||
|
* customise the controller.
|
||||||
|
*
|
||||||
|
* @param $params The parameters taken from the parsed URL of the parent url handler
|
||||||
|
* @param $request The {@link HTTPRequest} object that is reponsible for distributing URL parsing
|
||||||
|
* @uses HTTPRequest
|
||||||
|
*/
|
||||||
|
function handleRequest($request) {
|
||||||
|
foreach($this->stat('url_handlers') as $rule => $action) {
|
||||||
|
if($params = $request->match($rule, true)) {
|
||||||
|
|
||||||
|
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
|
||||||
|
if($action[0] == '$') $action = $params[substr($action,1)];
|
||||||
|
|
||||||
|
if($this->checkAccessAction($action)) {
|
||||||
|
$result = $this->$action($request);
|
||||||
|
} else {
|
||||||
|
return $this->httpError(403, "Action '$action' isn't allowed on class $this->class");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we return a RequestHandlingData, call handleRequest() on that, even if there is no more URL to parse.
|
||||||
|
// It might have its own handler. However, we only do this if we haven't just parsed an empty rule ourselves,
|
||||||
|
// to prevent infinite loops
|
||||||
|
if(!$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandlingData) {
|
||||||
|
$returnValue = $result->handleRequest($request);
|
||||||
|
|
||||||
|
// Array results can be used to handle
|
||||||
|
if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
|
||||||
|
|
||||||
|
return $returnValue;
|
||||||
|
|
||||||
|
// If we return some other data, and all the URL is parsed, then return that
|
||||||
|
} else if($request->allParsed()) {
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
// But if we have more content on the URL and we don't know what to do with it, return an error.
|
||||||
|
} else {
|
||||||
|
return $this->httpError(400, "I can't handle sub-URLs of a $this->class object.");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing matches, return this object
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the given action is allowed to be called from a URL.
|
||||||
|
* It will interrogate {@link self::$allowed_actions} to determine this.
|
||||||
|
*/
|
||||||
|
function checkAccessAction($action) {
|
||||||
|
// Collate self::$allowed_actions from this class and all parent classes
|
||||||
|
$access = null;
|
||||||
|
$className = $this->class;
|
||||||
|
while($className != 'RequestHandlingData') {
|
||||||
|
// Merge any non-null parts onto $access.
|
||||||
|
$accessPart = eval("return $className::\$allowed_actions;");
|
||||||
|
if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
|
||||||
|
|
||||||
|
// Build an array of parts for checking if part[0] == part[1], which means that this class doesn't directly define it.
|
||||||
|
$accessParts[] = $accessPart;
|
||||||
|
|
||||||
|
$className = get_parent_class($className);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add $allowed_actions from extensions
|
||||||
|
if($this->extension_instances) {
|
||||||
|
foreach($this->extension_instances as $inst) {
|
||||||
|
$accessPart = $inst->stat('allowed_actions');
|
||||||
|
if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($access === null || (isset($accessParts[1]) && $accessParts[0] === $accessParts[1])) {
|
||||||
|
// user_error("Deprecated: please define static \$allowed_actions on your Controllers for security purposes", E_USER_NOTICE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($action == 'index') return true;
|
||||||
|
|
||||||
|
if(isset($access[$action])) {
|
||||||
|
$test = $access[$action];
|
||||||
|
if($test === true) return true;
|
||||||
|
if(substr($test,0,2) == '->') {
|
||||||
|
$funcName = substr($test,2);
|
||||||
|
return $this->$funcName();
|
||||||
|
}
|
||||||
|
if(Permission::check($test)) return true;
|
||||||
|
} else if((($key = array_search($action, $access)) !== false) && is_numeric($key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an HTTP error instead of performing the normal processing
|
||||||
|
* @todo This doesn't work properly right now. :-(
|
||||||
|
*/
|
||||||
|
function httpError($errorCode, $errorMessage = null) {
|
||||||
|
$r = new HTTPResponse();
|
||||||
|
$r->setBody($errorMessage);
|
||||||
|
$r->setStatuscode($errorCode);
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@
|
|||||||
* @package forms
|
* @package forms
|
||||||
* @subpackage core
|
* @subpackage core
|
||||||
*/
|
*/
|
||||||
class Form extends ViewableData {
|
class Form extends RequestHandlingData {
|
||||||
|
|
||||||
public static $backup_post_data = false;
|
public static $backup_post_data = false;
|
||||||
|
|
||||||
@ -116,6 +116,80 @@ class Form extends ViewableData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static $url_handlers = array(
|
||||||
|
'POST ' => 'httpSubmission',
|
||||||
|
'GET ' => 'httpSubmission',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a form submission. GET and POST requests behave identically
|
||||||
|
*/
|
||||||
|
function httpSubmission($request) {
|
||||||
|
$vars = $request->requestVars();
|
||||||
|
|
||||||
|
if(isset($funcName)) {
|
||||||
|
Form::set_current_action($funcName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the form
|
||||||
|
$this->loadDataFrom($vars, true);
|
||||||
|
|
||||||
|
// Validate the form
|
||||||
|
if(!$this->validate()) {
|
||||||
|
if(Director::is_ajax()) {
|
||||||
|
return FormResponse::respond();
|
||||||
|
} else {
|
||||||
|
Director::redirectBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protection against CSRF attacks
|
||||||
|
if($this->securityTokenEnabled()) {
|
||||||
|
$securityID = Session::get('SecurityID');
|
||||||
|
|
||||||
|
if(!$securityID || !isset($vars['SecurityID']) || $securityID != $vars['SecurityID']) {
|
||||||
|
$this->httpError(400, "SecurityID doesn't match, possible CRSF attack.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the action button clicked
|
||||||
|
$funcName = null;
|
||||||
|
foreach($vars as $paramName => $paramVal) {
|
||||||
|
if(substr($paramName,0,7) == 'action_') {
|
||||||
|
// Break off querystring arguments included in the action
|
||||||
|
if(strpos($paramName,'?') !== false) {
|
||||||
|
list($paramName, $paramVars) = explode('?', $paramName, 2);
|
||||||
|
$newRequestParams = array();
|
||||||
|
parse_str($paramVars, $newRequestParams);
|
||||||
|
$vars = array_merge((array)$vars, (array)$newRequestParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup action_, _x and _y from image fields
|
||||||
|
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the action wasnt' set, choose the default on the form.
|
||||||
|
if(!isset($funcName) && $defaultAction = $this->defaultAction()){
|
||||||
|
$funcName = $defaultAction->actionName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($funcName)) {
|
||||||
|
$this->setButtonClicked($funcName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, try a handler method on the controller
|
||||||
|
if($this->controller->hasMethod($funcName)) {
|
||||||
|
return $this->controller->$funcName($vars, $this);
|
||||||
|
|
||||||
|
// Otherwise, try a handler method on the form object
|
||||||
|
} else {
|
||||||
|
return $this->$funcName($vars, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert this form into a readonly form
|
* Convert this form into a readonly form
|
||||||
*/
|
*/
|
||||||
@ -385,15 +459,10 @@ class Form extends ViewableData {
|
|||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
function FormAction() {
|
function FormAction() {
|
||||||
// "get" form needs ?executeForm added as a hidden field
|
if($this->controller->hasMethod("FormObjectLink")) {
|
||||||
if($this->formMethod == 'post') {
|
return $this->controller->FormObjectLink($this->name);
|
||||||
if($this->controller->hasMethod("FormObjectLink")) {
|
|
||||||
return $this->controller->FormObjectLink($this->name);
|
|
||||||
} else {
|
|
||||||
return $this->controller->Link() . "?executeForm=" . $this->name;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return $this->controller->Link();
|
return $this->controller->Link() . $this->name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,7 +578,7 @@ class Form extends ViewableData {
|
|||||||
* This includes form validation, if it fails, we redirect back
|
* This includes form validation, if it fails, we redirect back
|
||||||
* to the form with appropriate error messages
|
* to the form with appropriate error messages
|
||||||
*/
|
*/
|
||||||
function beforeProcessing(){
|
function validate(){
|
||||||
if($this->validator){
|
if($this->validator){
|
||||||
$errors = $this->validator->validate();
|
$errors = $this->validator->validate();
|
||||||
|
|
||||||
@ -526,7 +595,6 @@ class Form extends ViewableData {
|
|||||||
Convert::raw2js($error['messageType'])
|
Convert::raw2js($error['messageType'])
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
echo FormResponse::respond();
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
$data = $this->getData();
|
$data = $this->getData();
|
||||||
@ -541,7 +609,6 @@ class Form extends ViewableData {
|
|||||||
'data' => $data,
|
'data' => $data,
|
||||||
));
|
));
|
||||||
|
|
||||||
Director::redirectBack();
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* @package forms
|
* @package forms
|
||||||
* @subpackage core
|
* @subpackage core
|
||||||
*/
|
*/
|
||||||
class FormField extends ViewableData {
|
class FormField extends RequestHandlingData {
|
||||||
protected $form;
|
protected $form;
|
||||||
protected $name, $title, $value ,$message, $messageType, $extraClass;
|
protected $name, $title, $value ,$message, $messageType, $extraClass;
|
||||||
|
|
||||||
|
@ -542,6 +542,7 @@ class Member extends DataObject {
|
|||||||
parent::onAfterWrite();
|
parent::onAfterWrite();
|
||||||
|
|
||||||
if(isset($this->changed['Password']) && $this->changed['Password']) {
|
if(isset($this->changed['Password']) && $this->changed['Password']) {
|
||||||
|
$_REQUEST['showqueries'] = 1;
|
||||||
MemberPassword::log($this);
|
MemberPassword::log($this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,7 +269,7 @@ class Security extends Controller {
|
|||||||
$tmpPage->URLSegment = "Security";
|
$tmpPage->URLSegment = "Security";
|
||||||
$tmpPage->ID = -1; // Set the page ID to -1 so we dont get the top level pages as its children
|
$tmpPage->ID = -1; // Set the page ID to -1 so we dont get the top level pages as its children
|
||||||
|
|
||||||
$controller = new Page_Controller($tmpPage);
|
$controller = new Page_Controller($this->urlParams, $this->urlTokeniser, $tmpPage);
|
||||||
$controller->init();
|
$controller->init();
|
||||||
//Controller::$currentController = $controller;
|
//Controller::$currentController = $controller;
|
||||||
|
|
||||||
|
82
tests/ControllerTest.php
Normal file
82
tests/ControllerTest.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class ControllerTest extends SapphireTest {
|
||||||
|
static $fixture_file = null;
|
||||||
|
|
||||||
|
function testDefaultAction() {
|
||||||
|
/* For a controller with a template, the default action will simple run that template. */
|
||||||
|
$response = Director::test("ControllerTest_Controller/");
|
||||||
|
$this->assertEquals("This is the main template. Content is 'default content'.", $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMethodActions() {
|
||||||
|
/* The Action can refer to a method that is called on the object. If a method returns an array, then it will be
|
||||||
|
used to customise the template data */
|
||||||
|
$response = Director::test("ControllerTest_Controller/methodaction");
|
||||||
|
$this->assertEquals("This is the main template. Content is 'methodaction content'.", $response->getBody());
|
||||||
|
|
||||||
|
/* If the method just returns a string, then that will be used as the response */
|
||||||
|
$response = Director::test("ControllerTest_Controller/stringaction");
|
||||||
|
$this->assertEquals("stringaction was called.", $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testTemplateActions() {
|
||||||
|
/* If there is no method, it can be used to point to an alternative template. */
|
||||||
|
$response = Director::test("ControllerTest_Controller/templateaction");
|
||||||
|
$this->assertEquals("This is the template for templateaction. Content is 'default content'.", $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAllowedActions() {
|
||||||
|
$response = Director::test("ControllerTest_SecuredController/methodaction");
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$response = Director::test("ControllerTest_SecuredController/stringaction");
|
||||||
|
$this->assertEquals(403, $response->getStatusCode());
|
||||||
|
|
||||||
|
$response = Director::test("ControllerTest_SecuredController/adminonly");
|
||||||
|
$this->assertEquals(403, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple controller for testing
|
||||||
|
*/
|
||||||
|
class ControllerTest_Controller extends Controller {
|
||||||
|
public $Content = "default content";
|
||||||
|
|
||||||
|
function methodaction() {
|
||||||
|
return array(
|
||||||
|
"Content" => "methodaction content"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringaction() {
|
||||||
|
return "stringaction was called.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller with an $allowed_actions value
|
||||||
|
*/
|
||||||
|
class ControllerTest_SecuredController extends Controller {
|
||||||
|
static $allowed_actions = array(
|
||||||
|
"methodaction",
|
||||||
|
"adminonly" => "ADMIN",
|
||||||
|
);
|
||||||
|
|
||||||
|
public $Content = "default content";
|
||||||
|
|
||||||
|
function methodaction() {
|
||||||
|
return array(
|
||||||
|
"Content" => "methodaction content"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringaction() {
|
||||||
|
return "stringaction was called.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminonly() {
|
||||||
|
return "You must be an admin!";
|
||||||
|
}
|
||||||
|
}
|
154
tests/RequestHandlingTest.php
Normal file
154
tests/RequestHandlingTest.php
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for RequestHandlingData and HTTPRequest.
|
||||||
|
* We've set up a simple URL handling model based on
|
||||||
|
*/
|
||||||
|
class RequestHandlingTest extends SapphireTest {
|
||||||
|
static $fixture_file = null;
|
||||||
|
|
||||||
|
function testMethodCallingOnController() {
|
||||||
|
/* Calling a controller works just like it always has */
|
||||||
|
$response = Director::test("testGoodBase1");
|
||||||
|
$this->assertEquals("This is the controller", $response->getBody());
|
||||||
|
|
||||||
|
/* ID and OtherID are extracted from the URL and passed in $request->params. */
|
||||||
|
$response = Director::test("testGoodBase1/method/1/2");
|
||||||
|
$this->assertEquals("This is a method on the controller: 1, 2", $response->getBody());
|
||||||
|
|
||||||
|
/* In addition, these values are availalbe in $controller->urlParams. This is mainly for backward compatability. */
|
||||||
|
$response = Director::test("testGoodBase1/legacymethod/3/4");
|
||||||
|
$this->assertEquals("\$this->urlParams can be used, for backward compatibility: 3, 4", $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPostRequests() {
|
||||||
|
/* The HTTP Request handler can trigger special behaviour for GET and POST. */
|
||||||
|
$response = Director::test("testGoodBase1/TestForm", array("MyField" => 3), null, "POST");
|
||||||
|
$this->assertEquals("Form posted", $response->getBody());
|
||||||
|
|
||||||
|
$response = Director::test("testGoodBase1/TestForm");
|
||||||
|
$this->assertEquals("Get request on form", $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRequestHandlerChaining() {
|
||||||
|
/* Request handlers can be chained, from Director to Controller to Form to FormField. Here, we can make a get
|
||||||
|
request on a FormField. */
|
||||||
|
$response = Director::test("testGoodBase1/TestForm/fields/MyField");
|
||||||
|
$this->assertEquals("MyField requested", $response->getBody());
|
||||||
|
|
||||||
|
/* We can also make a POST request on a form field, which could be used for in-place editing, for example. */
|
||||||
|
$response = Director::test("testGoodBase1/TestForm/fields/MyField" ,array("MyField" => 5));
|
||||||
|
$this->assertEquals("MyField posted, update to 5", $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBadBase() {
|
||||||
|
/* Without a double-slash indicator in the URL, the entire URL is popped off the stack. The controller's default
|
||||||
|
action handlers have been designed for this to an extend: simple actions can still be called. This is the set-up
|
||||||
|
of URL rules written before this new request handler. */
|
||||||
|
$response = Director::test("testBadBase/method/1/2");
|
||||||
|
$this->assertEquals("This is a method on the controller: 1, 2", $response->getBody());
|
||||||
|
|
||||||
|
$response = Director::test("testBadBase/TestForm", array("MyField" => 3), null, "POST");
|
||||||
|
$this->assertEquals("Form posted", $response->getBody());
|
||||||
|
|
||||||
|
/* It won't, however, let you chain requests to access methods on forms, or form fields. In order to do that,
|
||||||
|
you need to have a // marker in your URL parsing rule */
|
||||||
|
$response = Director::test("testBadBase/TestForm/fields/MyField");
|
||||||
|
$this->assertNotEquals("MyField requested", $response->getBody());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Director rules for the test
|
||||||
|
*/
|
||||||
|
Director::addRules(50, array(
|
||||||
|
// If we don't request any variables, then the whole URL will get shifted off. This is fine, but it means that the
|
||||||
|
// controller will have to parse the Action from the URL itself.
|
||||||
|
'testGoodBase1' => "RequestHandlingTest_Controller",
|
||||||
|
|
||||||
|
// The double-slash indicates how much of the URL should be shifted off the stack. This is important for dealing
|
||||||
|
// with nested request handlers appropriately.
|
||||||
|
'testGoodBase2//$Action/$ID/$OtherID' => "RequestHandlingTest_Controller",
|
||||||
|
|
||||||
|
// By default, the entire URL will be shifted off. This creates a bit of backward-incompatability, but makes the
|
||||||
|
// URL rules much more explicit.
|
||||||
|
'testBadBase/$Action/$ID/$OtherID' => "RequestHandlingTest_Controller",
|
||||||
|
));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for the test
|
||||||
|
*/
|
||||||
|
class RequestHandlingTest_Controller extends Controller {
|
||||||
|
static $url_handlers = array(
|
||||||
|
// The double-slash is need here to ensure that
|
||||||
|
'$Action//$ID/$OtherID' => "handleAction",
|
||||||
|
);
|
||||||
|
|
||||||
|
function index($request) {
|
||||||
|
return "This is the controller";
|
||||||
|
}
|
||||||
|
|
||||||
|
function method($request) {
|
||||||
|
return "This is a method on the controller: " . $request->param('ID') . ', ' . $request->param('OtherID');
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacymethod($request) {
|
||||||
|
return "\$this->urlParams can be used, for backward compatibility: " . $this->urlParams['ID'] . ', ' . $this->urlParams['OtherID'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestForm() {
|
||||||
|
return new RequestHandlingTest_Form($this, "TestForm", new FieldSet(
|
||||||
|
new RequestHandlingTest_FormField("MyField")
|
||||||
|
), new FieldSet(
|
||||||
|
new FormAction("myAction")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form for the test
|
||||||
|
*/
|
||||||
|
class RequestHandlingTest_Form extends Form {
|
||||||
|
static $url_handlers = array(
|
||||||
|
'fields/$FieldName' => 'handleField',
|
||||||
|
"POST " => "handleSubmission",
|
||||||
|
"GET " => "handleGet",
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleField($request) {
|
||||||
|
return $this->dataFieldByName($request->param('FieldName'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmission($request) {
|
||||||
|
return "Form posted";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGet($request) {
|
||||||
|
return "Get request on form";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form field for the test
|
||||||
|
*/
|
||||||
|
class RequestHandlingTest_FormField extends FormField {
|
||||||
|
static $url_handlers = array(
|
||||||
|
"POST " => "handleInPlaceEdit",
|
||||||
|
'' => 'handleField',
|
||||||
|
'$Action' => '$Action',
|
||||||
|
);
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
return "Test method on $this->name";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleField() {
|
||||||
|
return "$this->name requested";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInPlaceEdit($request) {
|
||||||
|
return "$this->name posted, update to " . $request->postVar($this->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
tests/templates/ControllerTest.ss
Normal file
1
tests/templates/ControllerTest.ss
Normal file
@ -0,0 +1 @@
|
|||||||
|
This is the main template. Content is '$Content'.
|
1
tests/templates/ControllerTest_templateaction.ss
Normal file
1
tests/templates/ControllerTest_templateaction.ss
Normal file
@ -0,0 +1 @@
|
|||||||
|
This is the template for templateaction. Content is '$Content'.
|
Loading…
x
Reference in New Issue
Block a user