diff --git a/_config.php b/_config.php index e663c383427..a4e2fded82f 100644 --- a/_config.php +++ b/_config.php @@ -19,7 +19,7 @@ // Default director Director::addRules(10, array( - 'Security' => 'Security', + 'Security//$Action/$ID/$OtherID' => 'Security', //'Security/$Action/$ID' => 'Security', 'db/$Action' => 'DatabaseAdmin', '$Controller' => array( diff --git a/api/RestfulServer.php b/api/RestfulServer.php index 87d8a003322..279a8d25d5a 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -24,24 +24,29 @@ * * - POST /api/v1/(ClassName)/(ID)/(MethodName) - executes a method on the given object (e.g, publish) * + * @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers + * * @package sapphire * @subpackage api */ class RestfulServer extends Controller { static $url_handlers = array( - '$ClassName/#ID' => 'handleItem', - '$ClassName' => 'handleList', + '$ClassName/$ID/$Relation' => 'handleAction' + #'$ClassName/#ID' => 'handleItem', + #'$ClassName' => 'handleList', ); protected static $api_base = "api/v1/"; - function handleItem($params) { - return new RestfulServer_Item(DataObject::get_by_id($params["ClassName"], $params["ID"])); + /* + function handleItem($request) { + return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID"))); } - function handleList($params) { - return new RestfulServer_List(DataObject::get($params["ClassName"],"")); + function handleList($request) { + return new RestfulServer_List(DataObject::get($request->param("ClassName"),"")); } + */ /** * This handler acts as the switchboard for the controller. @@ -49,7 +54,7 @@ function handleList($params) { */ function index() { ContentNegotiator::disable(); - + $requestMethod = $_SERVER['REQUEST_METHOD']; if(!isset($this->urlParams['ClassName'])) return $this->notFound(); @@ -112,6 +117,12 @@ function index() { * * - static $api_access must be set. This enables the API on a class by class basis * - $obj->canView() must return true. This lets you implement record-level security + * + * @param String $className + * @param Int $id + * @param String $relation + * @param String $contentType + * @return String The serialized representation of the requested object(s) - usually XML or JSON. */ protected function getHandler($className, $id, $relation, $contentType) { if($id) { @@ -156,7 +167,11 @@ protected function getHandler($className, $id, $relation, $contentType) { } /** - * Generate an XML representation of the given DataObject. + * Generate an XML representation of the given {@link DataObject}. + * + * @param DataObject $obj + * @param $includeHeader Include header (Default: true) + * @return String XML */ protected function dataObjectAsXML(DataObject $obj, $includeHeader = true) { $className = $obj->class; @@ -212,22 +227,30 @@ protected function dataObjectAsXML(DataObject $obj, $includeHeader = true) { } /** - * Generate an XML representation of the given DataObject. + * Generate an XML representation of the given {@link DataObjectSet}. + * + * @param DataObjectSet $set + * @return String XML */ protected function dataObjectSetAsXML(DataObjectSet $set) { $className = $set->class; - $json = "\n<$className>\n"; + $xml = "\n<$className>\n"; foreach($set as $item) { - if($item->canView()) $json .= $this->dataObjectAsXML($item, false); + if($item->canView()) $xml .= $this->dataObjectAsXML($item, false); } - $json .= ""; + $xml .= ""; - return $json; + return $xml; } /** - * Generate an XML representation of the given DataObject. + * Generate an JSON representation of the given {@link DataObject}. + * + * @see http://json.org + * + * @param DataObject $obj + * @return String JSON */ protected function dataObjectAsJSON(DataObject $obj) { $className = $obj->class; @@ -278,7 +301,10 @@ protected function dataObjectAsJSON(DataObject $obj) { } /** - * Generate an XML representation of the given DataObject. + * Generate an JSON representation of the given {@link DataObjectSet}. + * + * @param DataObjectSet $set + * @return String JSON */ protected function dataObjectSetAsJSON(DataObjectSet $set) { $jsonParts = array(); @@ -346,8 +372,8 @@ function __construct($list) { $this->list = $list; } - function handleItem($params) { - return new RestulServer_Item($this->list->getById($params['ID'])); + function handleItem($request) { + return new RestulServer_Item($this->list->getById($request->param('ID'))); } } @@ -363,11 +389,11 @@ function __construct($item) { $this->item = $item; } - function handleRelation($params) { - $funcName = $params['Relation']; + function handleRelation($request) { + $funcName = $request('Relation'); $relation = $this->item->$funcName(); if($relation instanceof DataObjectSet) return new RestfulServer_List($relation); - else return new RestfulServer_Item($relation)l + else return new RestfulServer_Item($relation); } } diff --git a/api/RestfulService.php b/api/RestfulService.php index f184b0e53af..e38f7dc566b 100644 --- a/api/RestfulService.php +++ b/api/RestfulService.php @@ -284,11 +284,11 @@ function getValues($xml, $collection=NULL, $element=NULL){ $childElements = $xml->{$collection}->{$element}; if($childElements){ - foreach($childElements as $child){ - $data = array(); - $this->getRecurseValues($child,$data); - $output->push(new ArrayData($data)); - } + foreach($childElements as $child){ + $data = array(); + $this->getRecurseValues($child,$data); + $output->push(new ArrayData($data)); + } } return $output; } diff --git a/core/control/Controller.php b/core/control/Controller.php index d7e2b25b965..58af2f76bca 100644 --- a/core/control/Controller.php +++ b/core/control/Controller.php @@ -39,7 +39,7 @@ class Controller extends RequestHandlingData { * Default URL handlers - (Action)/(ID)/(OtherID) */ static $url_handlers = array( - '$Action/$ID/$OtherID' => 'handleAction', + '$Action//$ID/$OtherID' => 'handleAction', ); static $allowed_actions = array( diff --git a/core/control/HTTPRequest.php b/core/control/HTTPRequest.php index b0900a5804c..56e6bd13384 100644 --- a/core/control/HTTPRequest.php +++ b/core/control/HTTPRequest.php @@ -8,7 +8,7 @@ * match() to get the information that they need out of the URL. This is generally handled by * {@link RequestHandlingData::handleRequest()}. */ -class HTTPRequest extends Object { +class HTTPRequest extends Object implements ArrayAccess { /** * The non-extension parts of the URL, separated by "/" */ @@ -52,6 +52,39 @@ function requestVar($name) { if(isset($this->postVars[$name])) return $this->postVars[$name]; if(isset($this->getVars[$name])) return $this->getVars[$name]; } + + /** + * Enables the existence of a key-value pair in the request to be checked using + * array syntax, so isset($request['title']) will check for $_POST['title'] and $_GET['title] + * + * @param unknown_type $offset + * @return boolean + */ + function offsetExists($offset) { + if(isset($this->postVars[$offset])) return true; + if(isset($this->getVars[$offset])) return true; + return false; + } + + /** + * Access a request variable using array syntax. eg: $request['title'] instead of $request->postVar('title') + * + * @param unknown_type $offset + * @return unknown + */ + function offsetGet($offset) { + return $this->requestVar($offset); + } + + /** + * @ignore + */ + function offsetSet($offset, $value) {} + + /** + * @ignore + */ + function offsetUnset($offset) {} /** * Construct a HTTPRequest from a URL relative to the site root. @@ -159,7 +192,7 @@ function match($pattern, $shiftOnSuccess = false) { // 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; + if($v || !isset($this->allParams[$k])) $this->allParams[$k] = $v; } return $arguments; diff --git a/core/control/RequestHandlingData.php b/core/control/RequestHandlingData.php index b940688a8de..d6217691edd 100644 --- a/core/control/RequestHandlingData.php +++ b/core/control/RequestHandlingData.php @@ -69,7 +69,9 @@ class RequestHandlingData extends ViewableData { */ function handleRequest($request) { foreach($this->stat('url_handlers') as $rule => $action) { + if(isset($_GET['debug_request'])) Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class"); if($params = $request->match($rule, true)) { + if(isset($_GET['debug_request'])) Debug::message("Rule '$rule' matched on $this->class"); // Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action', if($action[0] == '$') $action = $params[substr($action,1)]; diff --git a/core/control/RootURLController.php b/core/control/RootURLController.php index 4e4468151b4..5ac2ac287e8 100755 --- a/core/control/RootURLController.php +++ b/core/control/RootURLController.php @@ -7,23 +7,18 @@ */ class RootURLController extends Controller { protected static $is_at_root = false; - - public function run($requestParams) { + + public function handleRequest($request) { self::$is_at_root = true; - - $this->pushCurrent(); - $controller = new ModelAsController(); - $controller->setUrlParams(array( - 'URLSegment' => self::get_homepage_urlsegment(), - 'Action' => '', - )); - $result = $controller->run($requestParams); + $controller = new ModelAsController(); - $this->popCurrent(); - return $result; + $request = new HTTPRequest("GET", self::get_homepage_urlsegment().'/', $request->getVars(), $request->postVars()); + $request->match('$URLSegment//$Action'); + + return $controller->handleRequest($request); } - + /** * Return the URL segment for the current HTTP_HOST value */ diff --git a/core/model/DataObject.php b/core/model/DataObject.php index d22ba010ab4..89588aeddda 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -616,6 +616,7 @@ public function write($showDebug = false, $forceInsert = false, $forceWrite = fa foreach($this->record as $fieldName => $fieldValue) { if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->fieldExists($fieldName)) { $fieldObj = $this->obj($fieldName); + $fieldObj->setValue($this->record[$fieldName], $this->record); if(!isset($manipulation[$class])) $manipulation[$class] = array(); if($fieldObj) $fieldObj->writeToManipulation($manipulation[$class]); } diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index f0023c60841..767e60fbef3 100755 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -40,7 +40,7 @@ class ComplexTableField extends TableListField { /** * @var string Classname of the parent-relation to correctly link new records. */ - protected $parentClass; + public $parentClass; /** * @var string Database column name for the used relation (e.g. FamilyID @@ -71,7 +71,7 @@ class ComplexTableField extends TableListField { * * @var string */ - protected $templatePopup = "ComplexTableField_popup"; + public $templatePopup = "ComplexTableField_popup"; /** * Classname for each row/item @@ -110,6 +110,20 @@ class ComplexTableField extends TableListField { * @var boolean */ protected $relationAutoSetting = true; + + + static $url_handlers = array( + 'item/$ID' => 'handleItem', + ); + + function handleItem($request) { + return new ComplexTableField_ItemRequest($this, $request->param('ID')); + } + + function getViewer() { + return new SSViewer('ComplexTableField'); + } + /** * See class comments @@ -208,6 +222,10 @@ function sourceItems() { return $this->sourceItems; } + + function sourceClass() { + return $this->sourceClass; + } /** * @return DataObjectSet @@ -240,121 +258,161 @@ function setPopupCaption($caption) { } /** - * Renders view, edit and add, depending on the given information. - * The form needs several parameters to function independently of its "parent-form", some derived from the context into a hidden-field, - * some derived from the parent context (which is not accessible here) and delivered by GET: - * ID, Identifier of the currently edited record (only if record is loaded). - * , Link back to the correct parent record (e.g. "parentID"). - * parentClass, Link back to correct container-class (the parent-record might have many 'has-one'-relationships) - * CAUTION: "ID" in the DetailForm would be the "childID" in the overview table. - * - * @param int $childID + * @param $validator Validator */ - function DetailForm($childID = null) { + function setDetailFormValidator( Validator $validator ) { + $this->detailFormValidator = $validator; + } + + /** + * Returns the content of this formfield without surrounding layout. Triggered by Javascript + * to update content after a DetailForm-save-action. + * + * @return String + */ + function ajax_render() { + return $this->renderWith($this->template); + } - // Get all the requests - $ID = isset($_REQUEST['ctf']['ID']) ? Convert::raw2xml($_REQUEST['ctf']['ID']) : null; - if(!isset($childID)) $childID = isset($_REQUEST['ctf']['childID']) ? Convert::raw2xml($_REQUEST['ctf']['childID']) : null; - $childClass = Convert::raw2xml($_REQUEST['fieldName']); - $this->methodName = isset($_REQUEST['methodName']) ? $_REQUEST['methodName'] : null; + /** + * Calculates the number of columns needed for colspans + * used in template + * + * @return Int + */ + function ItemCount() { + return count($this->fieldList); + } - // used to discover fields if requested and for population of field - if(is_numeric($childID)) { - // we have to use the basedataclass, otherwise we might exclude other subclasses - $childData = DataObject::get_by_id(ClassInfo::baseDataClass($this->sourceClass), $childID); - } - - // If the fieldset is passed, use it, else use the formfields returned - // from the object via a string method call. - if(is_a($this->detailFormFields,"Fieldset")){ - $detailFields = clone $this->detailFormFields; - } else if( isset( $childData ) && is_string($this->detailFormFields)){ - $functioncall = $this->detailFormFields; - if($childData->hasMethod($functioncall)){ - $detailFields = $childData->$functioncall(); - } - } elseif(! isset( $childData ) || $this->methodName == 'add') { - $SNG_sourceClass = singleton($this->sourceClass); - if(is_numeric($ID) && $this->getParentClass()) { - // make sure the relation-link is existing, even if we just add the sourceClass - // and didn't save it - $parentIDName = $this->getParentIdName( $this->getParentClass(), $this->sourceClass ); - $SNG_sourceClass->$parentIDName = $ID; - } - $functioncall = $this->detailFormFields; - if($SNG_sourceClass->hasMethod($functioncall)){ - $detailFields = $SNG_sourceClass->$functioncall(); - } - else - $detailFields = $SNG_sourceClass->getCMSFields(); - } else { - $detailFields = $childData->getCMSFields(); - } - - if($this->getParentClass()) { - $parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass); - if(!$parentIdName) { - user_error("ComplexTableField::DetailForm() Cannot automatically - determine 'has-one'-relationship to parent, - please use setParentClass() to set it manually", - E_USER_WARNING); - return; - } - // add relational fields - $detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass())); - - if( $this->relationAutoSetting ) - $detailFields->push(new HiddenField("$parentIdName"," ",$ID)); + /** + * Used to toggle paging (makes no sense when adding a record) + * + * @return Boolean + */ + function IsAddMode() { + return ($this->methodName == "add"); + } + + function sourceID() { + $idField = $this->form->dataFieldByName('ID'); + if(!$idField) { + user_error("ComplexTableField needs a formfield named 'ID' to be present", E_USER_ERROR); } + // because action_callfieldmethod never actually loads data into the form, + // we can't rely on $idField being populated, and fall back to the request-params. + // this is a workaround for a bug where each subsequent popup-call didn't have ID + // of the parent set, and so didn't properly save the relation + return ($idField->Value()) ? $idField->Value() : (isset($_REQUEST['ctf']['ID']) ? $_REQUEST['ctf']['ID'] : null); + } + - // the ID field confuses the Controller-logic in finding the right view for ReferencedField - $detailFields->removeByName('ID'); - - // only add childID if we're not adding a record - if($this->methodName != 'add') { - $detailFields->push(new HiddenField("ctf[childID]","",$childID)); - } - - // add a namespaced ID instead thats "converted" by saveComplexTableField() - $detailFields->push(new HiddenField("ctf[ClassName]","",$this->sourceClass)); - - $readonly = ($this->methodName == "show"); - // if no custom validator is set, and there's on present on the object (e.g. Member), use it - if(!isset($this->detailFormValidator) && singleton($this->sourceClass)->hasMethod('getValidator')) { - $this->detailFormValidator = singleton($this->sourceClass)->getValidator(); - } + function AddLink() { + return $this->Link() . '/add'; + } - $form = Object::create($this->popupClass,$this, "DetailForm", $detailFields, $this->sourceClass, $readonly, $this->detailFormValidator); - - if (is_numeric($childID)) { - if ($this->methodName == "show" || $this->methodName == "edit") { - $form->loadDataFrom($childData); - } + /** + * @return FieldSet + */ + function createFieldSet() { + $fieldset = new FieldSet(); + foreach($this->fieldTypes as $key => $fieldType){ + $fieldset->push(new $fieldType($key)); } + return $fieldset; + } - if ($this->methodName == "show") { - $form->makeReadonly(); + /** + * Determines on which relation-class the DetailForm is saved + * by looking at the surrounding form-record. + * + * @return String + */ + function getParentClass() { + if($this->parentClass === false) { + // purposely set parent-relation to false + return false; + } elseif(!empty($this->parentClass)) { + return $this->parentClass; + } else { + return $this->form->getRecord()->ClassName; } + } - return $form; + /** + * (Optional) Setter for a correct parent-relation-class. + * Defaults to the record loaded into the surrounding form as a fallback. + * Caution: Please use the classname, not the actual column-name in the database. + * + * @param $className string + */ + function setParentClass($className) { + $this->parentClass = $className; } /** - * @param $validator Validator + * Returns the db-fieldname of the currently used has_one-relationship. */ - function setDetailFormValidator( Validator $validator ) { - $this->detailFormValidator = $validator; + function getParentIdName( $parentClass, $childClass ) { + return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' ); } /** - * Returns the content of this formfield without surrounding layout. Triggered by Javascript - * to update content after a DetailForm-save-action. - * - * @return String + * Manually overwrites the parent-ID relations. + * @see setParentClass() + * + * @param String $str Example: FamilyID (when one Individual has_one Family) */ - function ajax_render() { - return $this->renderWith($this->template); + function setParentIdName($str) { + $this->parentIdName = $str; + } + + /** + * Returns the db-fieldname of the currently used relationship. + */ + function getParentIdNameRelation( $parentClass, $childClass, $relation ){ + if($this->parentIdName) return $this->parentIdName; + + $relations = singleton( $parentClass )->$relation(); + $classes = ClassInfo::ancestry( $childClass ); + foreach( $relations as $k => $v ) { + if( $v == $childClass ) + return $k . 'ID'; + else if( array_key_exists( $v, $classes ) ) + return $classes[ $v ] . 'ID'; + } + return false; + } + + function setTemplatePopup($template) { + $this->templatePopup = $template; + } +} + +/** + * @todo Tie this into ComplexTableField_Item better. + */ +class ComplexTableField_ItemRequest extends RequestHandlingData { + protected $ctf; + protected $itemID; + protected $methodName; + + static $url_handlers = array( + '$Action!' => '$Action', + '' => 'index', + ); + + function Link() { + return $this->ctf->Link() . '/item/' . $this->itemID; + } + + function __construct($ctf, $itemID) { + $this->ctf = $ctf; + $this->itemID = $itemID; + } + + function index() { + return $this->show(); } /** @@ -363,21 +421,22 @@ function ajax_render() { * @return String */ function show() { - if($this->Can('show') !== true) { + if($this->ctf->Can('show') !== true) { return false; } - $this->methodName = "edit"; - - $this->sourceItems = $this->sourceItems(); + $this->methodName = "show"; + /* + $this->sourceItems = $this->ctg->sourceItems(); $this->pageSize = 1; if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) { $this->unpagedSourceItems->setPageLimits($_REQUEST['ctf'][$this->Name()]['start'], $this->pageSize, $this->totalCount); } + */ - echo $this->renderWith($this->templatePopup); + echo $this->renderWith($this->ctf->templatePopup); } /** @@ -386,12 +445,12 @@ function show() { * @return String */ function edit() { - if($this->Can('edit') !== true) { + if($this->ctf->Can('edit') !== true) { return false; } $this->methodName = "edit"; - + /* $this->sourceItems = $this->sourceItems(); $this->pageSize = 1; @@ -399,8 +458,9 @@ function edit() { if(is_numeric($_REQUEST['ctf']['start'])) { $this->unpagedSourceItems->setPageLimits($_REQUEST['ctf']['start'], $this->pageSize, $this->totalCount); } + */ - echo $this->renderWith($this->templatePopup); + echo $this->renderWith($this->ctf->templatePopup); } /** @@ -418,42 +478,117 @@ function add() { echo $this->renderWith($this->templatePopup); } + /////////////////////////////////////////////////////////////////////////////////////////////////// + /** - * Calculates the number of columns needed for colspans - * used in template - * - * @return Int + * Return the data object being manipulated */ - function ItemCount() { - return count($this->fieldList); + function obj() { + // used to discover fields if requested and for population of field + if(is_numeric($this->itemID)) { + // we have to use the basedataclass, otherwise we might exclude other subclasses + return DataObject::get_by_id(ClassInfo::baseDataClass($this->ctf->sourceClass()), $this->itemID); + } + } /** - * Used to toggle paging (makes no sense when adding a record) - * - * @return Boolean + * Renders view, edit and add, depending on the given information. + * The form needs several parameters to function independently of its "parent-form", some derived from the context into a hidden-field, + * some derived from the parent context (which is not accessible here) and delivered by GET: + * ID, Identifier of the currently edited record (only if record is loaded). + * , Link back to the correct parent record (e.g. "parentID"). + * parentClass, Link back to correct container-class (the parent-record might have many 'has-one'-relationships) + * CAUTION: "ID" in the DetailForm would be the "childID" in the overview table. + * + * @param int $childID */ - function IsAddMode() { - return ($this->methodName == "add"); + function DetailForm($childID = null) { + $childData = $this->obj(); + + // If the fieldset is passed, use it, else use the formfields returned + // from the object via a string method call. + if(is_a($this->ctf->detailFormFields,"Fieldset")){ + $detailFields = $this->detailFormFields; + + } else if( isset( $childData ) && is_string($this->ctf->detailFormFields)){ + $functioncall = $this->ctf->detailFormFields; + if($childData->hasMethod($functioncall)){ + $detailFields = $childData->$functioncall(); + } + + } elseif(! isset( $childData ) || $this->methodName == 'add') { + $SNG_sourceClass = singleton($this->ctf->sourceClass()); + if($childData && is_numeric($childData->ID) && $this->ctf->getParentClass()) { + // make sure the relation-link is existing, even if we just add the sourceClass + // and didn't save it + $parentIDName = $this->ctf->getParentIdName( $this->getParentClass(), $this->sourceClass() ); + $SNG_sourceClass->$parentIDName = $childData->ID; + } + $functioncall = $this->detailFormFields; + if($SNG_sourceClass->hasMethod($functioncall)){ + $detailFields = $SNG_sourceClass->$functioncall(); + } + else + $detailFields = $SNG_sourceClass->getCMSFields(); + } else { + $detailFields = $childData->getCMSFields(); + } + + if($this->ctf->getParentClass()) { + $parentIdName = $this->ctf->getParentIdName($this->ctf->getParentClass(), $this->ctf->sourceClass()); + /* + if(!$parentIdName) { + user_error("ComplexTableField::DetailForm() Cannot automatically + determine 'has-one'-relationship to parent class " . $this->ctf->getParentClass() . ", + please use setParentClass() to set it manually", + E_USER_WARNING); + return; + } + */ + + if($parentIdName) { + // add relational fields + $detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->ctf->getParentClass())); + + if( $this->relationAutoSetting ) + $detailFields->push(new HiddenField("$parentIdName"," ",$childData->ID)); + } + } + + // the ID field confuses the Controller-logic in finding the right view for ReferencedField + $detailFields->removeByName('ID'); + + // only add childID if we're not adding a record + if($this->methodName != 'add') { + $detailFields->push(new HiddenField("ctf[childID]","",$childData->ID)); + } + + // add a namespaced ID instead thats "converted" by saveComplexTableField() + $detailFields->push(new HiddenField("ctf[ClassName]","",$this->ctf->sourceClass())); + + $readonly = ($this->methodName == "show"); + + // if no custom validator is set, and there's on present on the object (e.g. Member), use it + if(!isset($this->ctf->detailFormValidator) && singleton($this->ctf->sourceClass())->hasMethod('getValidator')) { + $this->ctf->detailFormValidator = singleton($this->ctf->sourceClass())->getValidator(); + } + + $form = Object::create($this->ctf->popupClass,$this, "DetailForm", $detailFields, $this->ctf->detailFormValidator, $readonly, $childData); + + if (is_numeric($childData->ID)) { + if ($this->methodName == "show" || $this->methodName == "edit") { + $form->loadDataFrom($childData); + } + } + + if ($this->methodName == "show") { + $form->makeReadonly(); + } + + return $form; } - function sourceID() { - $idField = $this->form->dataFieldByName('ID'); - if(!$idField) { - user_error("ComplexTableField needs a formfield named 'ID' to be present", E_USER_ERROR); - } - // because action_callfieldmethod never actually loads data into the form, - // we can't rely on $idField being populated, and fall back to the request-params. - // this is a workaround for a bug where each subsequent popup-call didn't have ID - // of the parent set, and so didn't properly save the relation - return ($idField->Value()) ? $idField->Value() : (isset($_REQUEST['ctf']['ID']) ? $_REQUEST['ctf']['ID'] : null); - } - - /** - * ################################# - * Pagination - * ################################# - */ function PopupBaseLink() { $link = $this->FormAction() . "&action_callfieldmethod&fieldName={$this->Name()}"; if(!strpos($link,'ctf[ID]')) { @@ -560,49 +695,8 @@ function PopupClasses() { return implode(" ", $items); } +} - function AddLink() { - return Convert::raw2att("{$this->PopupBaseLink()}&methodName=add"); - } - - /** - * @return FieldSet - */ - function createFieldSet() { - $fieldset = new FieldSet(); - foreach($this->fieldTypes as $key => $fieldType){ - $fieldset->push(new $fieldType($key)); - } - return $fieldset; - } - - /** - * Determines on which relation-class the DetailForm is saved - * by looking at the surrounding form-record. - * - * @return String - */ - function getParentClass() { - if($this->parentClass === false) { - // purposely set parent-relation to false - return false; - } elseif(!empty($this->parentClass)) { - return $this->parentClass; - } else { - return $this->form->getRecord()->ClassName; - } - } - - /** - * (Optional) Setter for a correct parent-relation-class. - * Defaults to the record loaded into the surrounding form as a fallback. - * Caution: Please use the classname, not the actual column-name in the database. - * - * @param $className string - */ - function setParentClass($className) { - $this->parentClass = $className; - } /** * Returns the db-fieldname of the currently used has_one-relationship. @@ -659,20 +753,20 @@ function __construct(DataObject $item, ComplexTableField $parent, $start) { parent::__construct($item, $parent); } - function PopupBaseLink() { - return $this->parent->FormAction() . "&action_callfieldmethod&fieldName={$this->parent->Name()}&ctf[childID]={$this->item->ID}&ctf[ID]={$this->parent->sourceID()}&ctf[start]={$this->start}"; + function Link() { + return $this->parent->Link() . '/item/' . $this->item->ID; } function EditLink() { - return Convert::raw2att($this->PopupBaseLink() . "&methodName=edit"); + return $this->Link() . "/edit"; } function ShowLink() { - return Convert::raw2att($this->PopupBaseLink() . "&methodName=show"); + return $this->Link() . "/show"; } function DeleteLink() { - return Convert::raw2att($this->PopupBaseLink() . "&methodName=delete"); + return $this->Link() . "/delete"; } } @@ -687,8 +781,10 @@ function DeleteLink() { */ class ComplexTableField_Popup extends Form { protected $sourceClass; + protected $dataObject; - function __construct($controller, $name, $field, $sourceClass, $readonly=false, $validator = null) { + function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) { + $this->dataObject = $dataObject; /** * WARNING: DO NOT CHANGE THE ORDER OF THESE JS FILES @@ -715,9 +811,8 @@ function __construct($controller, $name, $field, $sourceClass, $readonly=false, Requirements::javascript("sapphire/javascript/ComplexTableField.js"); Requirements::javascript("sapphire/javascript/ComplexTableField_popup.js"); - $this->sourceClass = $sourceClass; - if(singleton($sourceClass)->hasMethod('getRequirementsForPopup')){ - singleton($sourceClass)->getRequirementsForPopup(); + if($this->dataObject->hasMethod('getRequirementsForPopup')) { + $this->data->getRequirementsForPopup(); } $actions = new FieldSet(); @@ -728,7 +823,7 @@ function __construct($controller, $name, $field, $sourceClass, $readonly=false, $saveAction->addExtraClass('save'); } - parent::__construct($controller, $name, $field, $actions, $validator); + parent::__construct($controller, $name, $fields, $actions, $validator); } function FieldHolder() { @@ -748,22 +843,15 @@ function ShowPagination() { * * @see {Form::ReferencedField}). */ - function saveComplexTableField() { - if(isset($_REQUEST['ctf']['childID']) && is_numeric($_REQUEST['ctf']['childID'])) { - $childObject = DataObject::get_by_id($this->sourceClass, $_REQUEST['ctf']['childID']); - } else { - $childObject = new $this->sourceClass(); - $this->fields->removeByName('ID'); - } - - $this->saveInto($childObject); - $childObject->write(); + function saveComplexTableField($params) { + $this->saveInto($this->dataObject); + $this->dataObject->write(); // if ajax-call in an iframe, update window if(Director::is_ajax()) { // Newly saved objects need their ID reflected in the reloaded form to avoid double saving - $form = $this->controller->DetailForm($childObject->ID); - $form->loadDataFrom($childObject); + $form = $this->controller->DetailForm(); + //$form->loadDataFrom($this->dataObject); FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update'); return FormResponse::respond(); } else { diff --git a/forms/Form.php b/forms/Form.php index 4990a2c4024..c7e7a68e43e 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -117,6 +117,7 @@ function __construct($controller, $name, FieldSet $fields, FieldSet $actions, $v } static $url_handlers = array( + 'field/$FieldName!' => 'handleField', 'POST ' => 'httpSubmission', 'GET ' => 'httpSubmission', ); @@ -126,7 +127,6 @@ function __construct($controller, $name, FieldSet $fields, FieldSet $actions, $v */ function httpSubmission($request) { $vars = $request->requestVars(); - if(isset($funcName)) { Form::set_current_action($funcName); } @@ -170,7 +170,7 @@ function httpSubmission($request) { break; } } - + // If the action wasnt' set, choose the default on the form. if(!isset($funcName) && $defaultAction = $this->defaultAction()){ $funcName = $defaultAction->actionName(); @@ -189,6 +189,13 @@ function httpSubmission($request) { return $this->$funcName($vars, $this); } } + + /** + * Handle a field request + */ + function handleField($request) { + return $this->dataFieldByName($request->param('FieldName')); + } /** * Convert this form into a readonly form @@ -461,8 +468,10 @@ function setFormMethod($method) { function FormAction() { if($this->controller->hasMethod("FormObjectLink")) { return $this->controller->FormObjectLink($this->name); - } else { - return $this->controller->Link() . $this->name; + } else { + $link = $this->controller->Link(); + if(substr($link,-1) != '/') $link .= '/'; + return $link . $this->name; } } diff --git a/forms/FormField.php b/forms/FormField.php index 4f4d02e767c..e7e55ba0187 100644 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -60,6 +60,13 @@ function __construct($name, $title = null, $value = null, $form = null, $rightTi parent::__construct(); } + /** + * Return a Link to this field + */ + function Link() { + return $this->form->FormAction() . '/field/' . $this->name; + } + /** * Returns the HTML ID of the field - used in the template by label tags. * The ID is generated as FormName_FieldName. All Field functions should ensure diff --git a/javascript/ComplexTableField.js b/javascript/ComplexTableField.js index a4671737326..5646e51a21d 100755 --- a/javascript/ComplexTableField.js +++ b/javascript/ComplexTableField.js @@ -72,6 +72,8 @@ ComplexTableField.prototype = { popupLink = _popupLink; table = _table; } else { + alert(this.innerHTML); + // if clicked item is an input-element, don't trigger popup var el = Event.element(e); var input = Event.findElement(e,"input"); @@ -110,10 +112,6 @@ ComplexTableField.prototype = { } } - if($('SecurityID')) { - popupLink = popupLink + '&SecurityID=' + $('SecurityID').value; - } - GB_OpenerObj = this; // use same url to refresh the table after saving the popup, but use a generic rendering method GB_RefreshLink = popupLink; @@ -121,6 +119,7 @@ ComplexTableField.prototype = { // dont include pagination index GB_RefreshLink = GB_RefreshLink.replace(/ctf\[start\][^&]*/,""); GB_RefreshLink += '&forcehtml=1'; + if(this.GB_Caption) { var title = this.GB_Caption; } else { diff --git a/javascript/ComplexTableField_popup.js b/javascript/ComplexTableField_popup.js index 2b5fe1fad4d..20633e8717d 100755 --- a/javascript/ComplexTableField_popup.js +++ b/javascript/ComplexTableField_popup.js @@ -58,6 +58,7 @@ ComplexTableFieldPopupForm.prototype = { // don't update when validation is present and failed if(!this.validate || (this.validate && !hasHadFormError())) { + alert("GB:" + parent.parent.GB_RefreshLink); new parent.parent.Ajax.Request( parent.parent.GB_RefreshLink, { diff --git a/main.php b/main.php index 2d2d8838960..1aa6fd51e1c 100644 --- a/main.php +++ b/main.php @@ -140,7 +140,6 @@ // Direct away - this is the "main" function, that hands control to the appropriate controller if(isset($_GET['debug_profile'])) Profiler::unmark('main.php init'); - Director::direct($url); if(isset($_GET['debug_profile'])) { diff --git a/security/Security.php b/security/Security.php index 7ed8906f03d..92581f9dacd 100644 --- a/security/Security.php +++ b/security/Security.php @@ -269,7 +269,7 @@ public function login() { $tmpPage->URLSegment = "Security"; $tmpPage->ID = -1; // Set the page ID to -1 so we dont get the top level pages as its children - $controller = new Page_Controller($this->urlParams, $this->urlTokeniser, $tmpPage); + $controller = new Page_Controller($tmpPage); $controller->init(); //Controller::$currentController = $controller; diff --git a/templates/ComplexTableField_popup.ss b/templates/ComplexTableField_popup.ss index 40eac9ec00e..d672cf82a67 100755 --- a/templates/ComplexTableField_popup.ss +++ b/templates/ComplexTableField_popup.ss @@ -34,8 +34,9 @@ <% _t('NEXT', 'Next') %> <% end_if %> - - <% end_if %> + + <% end_if %> + <% end_if %>