Merge pull request #1250 from wilr/gridfield-action-fixes

FIX: Do not blindly pass input values to GridField_FormAction URL's
This commit is contained in:
Ingo Schommer 2013-05-08 04:20:40 -07:00
commit a1216b5e32
9 changed files with 290 additions and 148 deletions

View File

@ -81,6 +81,7 @@ Used in side panels and action tabs
.cms table.ss-gridfield-table tr th.extra { position: relative; background: #637276; background: rgba(0, 0, 0, 0.7); padding: 5px; border-top: rgba(0, 0, 0, 0.2); }
.cms table.ss-gridfield-table tr th.extra input { height: 28px; }
.cms table.ss-gridfield-table tr th.extra button.ss-ui-button { padding: .3em; line-height: 1; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; position: relative; border-bottom-width: 0; -webkit-border-radius: 2px 2px; -moz-border-radius: 2px / 2px; border-radius: 2px / 2px; }
.cms table.ss-gridfield-table tr th.extra select { margin: 0; }
.cms table.ss-gridfield-table tr th.first { -moz-border-radius-topleft: 5px; -webkit-border-top-left-radius: 5px; border-top-left-radius: 5px; }
.cms table.ss-gridfield-table tr th.last { -moz-border-radius-topright: 5px; -webkit-border-top-right-radius: 5px; border-top-right-radius: 5px; }
.cms table.ss-gridfield-table tr th button#action_gridfield_relationadd:hover { color: #444 !important; /* Not sure why IE think it needs this */ }

View File

@ -54,7 +54,9 @@ class GridField extends FormField {
protected $config = null;
/**
* The components list
* The components list
*
* @var array
*/
protected $components = array();
@ -68,9 +70,14 @@ class GridField extends FormField {
/**
* Map of callbacks for custom data fields
*
* @var array
*/
protected $customDataFields = array();
/**
* @var string
*/
protected $name = '';
/**
@ -222,12 +229,14 @@ class GridField extends FormField {
* Get the current GridState_Data or the GridState
*
* @param bool $getData - flag for returning the GridState_Data or the GridState
*
* @return GridState_data|GridState
*/
public function getState($getData=true) {
public function getState($getData = true) {
if($getData) {
return $this->state->getData();
}
return $this->state;
}
@ -465,11 +474,16 @@ class GridField extends FormField {
/**
* Add additional calculated data fields to be used on this GridField
* @param array $fields a map of fieldname to callback. The callback will bed passed the record as an argument.
*
* @param array $fields a map of fieldname to callback. The callback will
* be passed the record as an argument.
*/
public function addDataFields($fields) {
if($this->customDataFields) $this->customDataFields = array_merge($this->customDataFields, $fields);
else $this->customDataFields = $fields;
if($this->customDataFields) {
$this->customDataFields = array_merge($this->customDataFields, $fields);
} else {
$this->customDataFields = $fields;
}
}
/**
@ -580,9 +594,11 @@ class GridField extends FormField {
*/
protected function buildColumnDispatch() {
$this->columnDispatch = array();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_ColumnProvider) {
$columns = $item->getColumnsHandled($this);
foreach($columns as $column) {
$this->columnDispatch[$column][] = $item;
}
@ -603,14 +619,19 @@ class GridField extends FormField {
// Update state from client
$state = $this->getState(false);
if(isset($fieldData['GridState'])) $state->setValue($fieldData['GridState']);
if(isset($fieldData['GridState'])) {
$state->setValue($fieldData['GridState']);
}
// Try to execute alter action
foreach($data as $k => $v) {
if(preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $k, $matches)) {
$id = $matches[1];
$stateChange = Session::get($id);
$actionName = $stateChange['actionName'];
$args = isset($stateChange['args']) ? $stateChange['args'] : array();
$html = $this->handleAlterAction($actionName, $args, $data);
// A field can optionally return its own HTML
@ -750,31 +771,31 @@ class GridField extends FormField {
class GridField_FormAction extends FormAction {
/**
*
* @var GridField
*/
protected $gridField;
/**
*
* @var array
*/
protected $stateValues;
/**
*
* @var array
*/
//protected $stateFields = array();
protected $actionName;
protected $args = array();
/**
* @var string
*/
protected $actionName;
/**
* @var boolean
*/
public $useButtonTag = true;
/**
*
* @param GridField $gridField
* @param type $name
* @param type $label
@ -785,11 +806,13 @@ class GridField_FormAction extends FormAction {
$this->gridField = $gridField;
$this->actionName = $actionName;
$this->args = $args;
parent::__construct($name, $title);
}
/**
* urlencode encodes less characters in percent form than we need - we need everything that isn't a \w
* urlencode encodes less characters in percent form than we need - we
* need everything that isn't a \w.
*
* @param string $val
*/
@ -806,13 +829,17 @@ class GridField_FormAction extends FormAction {
return '%'.dechex(ord($match[0]));
}
/**
* @return array
*/
public function getAttributes() {
// Store state in session, and pass ID to client side
// Store state in session, and pass ID to client side.
$state = array(
'grid' => $this->getNameFromParent(),
'actionName' => $this->actionName,
'args' => $this->args,
);
$id = preg_replace('/[^\w]+/', '_', uniqid('', true));
Session::set($id, $state);
$actionData['StateID'] = $id;
@ -837,10 +864,12 @@ class GridField_FormAction extends FormAction {
protected function getNameFromParent() {
$base = $this->gridField;
$name = array();
do {
array_unshift($name, $base->getName());
$base = $base->getForm();
} while ($base && !($base instanceof Form));
return implode('.', $name);
}
}

View File

@ -119,7 +119,7 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
$fileData .= "\n";
}
$items = $gridField->getList();
$items = $gridField->getManipulatedList();
// @todo should GridFieldComponents change behaviour based on whether others are available in the config?
foreach($gridField->getConfig()->getComponents() as $component){

View File

@ -1,17 +1,18 @@
<?php
/**
* Adds an "Print" button to the bottom or top of a GridField.
*
* @package framework
* @subpackage gridfield
*/
/**
* Adds an "Print" button to the bottom or top of a GridField.
*/
class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionProvider, GridField_URLHandler {
/**
* @var array Map of a property name on the printed objects, with values being the column title in the CSV file.
* Note that titles are only used when {@link $csvHasHeader} is set to TRUE.
* @var array Map of a property name on the printed objects, with values
* being the column title in the CSV file.
*
* Note that titles are only used when {@link $csvHasHeader} is set to TRUE
*/
protected $printColumns;
@ -21,7 +22,9 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
protected $printHasHeader = true;
/**
* Fragment to write the button to
* Fragment to write the button to.
*
* @var string
*/
protected $targetFragment;
@ -36,6 +39,10 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
/**
* Place the print button in a <p> tag below the field
*
* @param GridField
*
* @return array
*/
public function getHTMLFragments($gridField) {
$button = new GridField_FormAction(
@ -45,21 +52,34 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
'print',
null
);
$button->setAttribute('data-icon', 'grid_print');
$button->addExtraClass('gridfield-button-print');
//$button->addExtraClass('no-ajax');
return array(
$this->targetFragment => '<p class="grid-print-button">' . $button->Field() . '</p>',
);
}
/**
* print is an action button
* Print is an action button.
*
* @param GridField
*
* @return array
*/
public function getActions($gridField) {
return array('print');
}
/**
* Handle the print action.
*
* @param GridField
* @param string
* @param array
* @param array
*/
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
if($actionName == 'print') {
return $this->handlePrint($gridField);
@ -67,7 +87,10 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
}
/**
* it is also a URL
* Print is accessible via the url
*
* @param GridField
* @return array
*/
public function getURLHandlers($gridField) {
return array(
@ -82,15 +105,20 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
set_time_limit(60);
Requirements::clear();
Requirements::css(FRAMEWORK_DIR . '/css/GridField_print.css');
if($data = $this->generatePrintData($gridField)){
return $data->renderWith("GridField_print");
}
}
/**
* Export core.
*/
public function generatePrintData($gridField) {
* Return the columns to print
*
* @param GridField
*
* @return array
*/
protected function getPrintColumnsForGridField(GridField $gridField) {
if($this->printColumns) {
$printColumns = $this->printColumns;
} else if($dataCols = $gridField->getConfig()->getComponentByType('GridFieldDataColumns')) {
@ -98,74 +126,93 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
} else {
$printColumns = singleton($gridField->getModelClass())->summaryFields();
}
$header = null;
if($this->printHasHeader){
$header = new ArrayList();
foreach($printColumns as $field => $label){
$header->push(
new ArrayData(array(
"CellString" => $label,
))
);
}
}
$items = $gridField->getList();
foreach($gridField->getConfig()->getComponents() as $component){
if($component instanceof GridFieldFilterHeader || $component instanceof GridFieldSortableHeader) {
$items = $component->getManipulatedData($gridField, $items);
}
}
$itemRows = new ArrayList();
foreach($items as $item) {
$itemRow = new ArrayList();
foreach($printColumns as $field => $label) {
$value = $gridField->getDataFieldValue($item, $field);
$itemRow->push(
new ArrayData(array(
"CellString" => $value,
))
);
}
$itemRows->push(new ArrayData(
array(
"ItemRow" => $itemRow
)
));
$item->destroy();
}
//get title for the print view
return $printColumns;
}
/**
* Return the title of the printed page
*
* @param GridField
*
* @return array
*/
public function getTitle(GridField $gridField) {
$form = $gridField->getForm();
$currentController = Controller::curr();
$title = '';
if(method_exists($currentController, 'Title')) {
$title = $currentController->Title();
}else{
if($currentController->Title){
} else {
if ($currentController->Title) {
$title = $currentController->Title;
}else{
} else {
if($form->Name()){
$title = $form->Name();
}
}
}
if($fieldTitle = $gridField->Title()){
if($title) $title .= " - ";
if($fieldTitle = $gridField->Title()) {
if($title) {
$title .= " - ";
}
$title .= $fieldTitle;
}
return $title;
}
/**
* Export core.
*
* @param GridField
*/
public function generatePrintData(GridField $gridField) {
$printColumns = $this->getPrintColumnsForGridField($gridField);
$ret = new ArrayData(
array(
"Title" => $title,
"Header" => $header,
"ItemRows" => $itemRows,
"Datetime" => SS_Datetime::now(),
"Member" => Member::currentUser(),
)
);
$header = null;
if($this->printHasHeader) {
$header = new ArrayList();
foreach($printColumns as $field => $label){
$header->push(new ArrayData(array(
"CellString" => $label,
)));
}
}
$items = $gridField->getManipulatedList();
$itemRows = new ArrayList();
foreach($items as $item) {
$itemRow = new ArrayList();
foreach($printColumns as $field => $label) {
$value = $gridField->getDataFieldValue($item, $field);
$itemRow->push(new ArrayData(array(
"CellString" => $value,
)));
}
$itemRows->push(new ArrayData(array(
"ItemRow" => $itemRow
)));
$item->destroy();
}
$ret = new ArrayData(array(
"Title" => $this->getTitle($gridField),
"Header" => $header,
"ItemRows" => $itemRows,
"Datetime" => SS_Datetime::now(),
"Member" => Member::currentUser(),
));
return $ret;
}
@ -182,6 +229,7 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
*/
public function setPrintColumns($cols) {
$this->printColumns = $cols;
return $this;
}
@ -197,6 +245,7 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
*/
public function setPrintHasHeader($bool) {
$this->printHasHeader = $bool;
return $this;
}
}
}

View File

@ -83,10 +83,12 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
$state = $gridField->State->GridFieldSortableHeader;
$columns = $gridField->getColumns();
$currentColumn = 0;
foreach($columns as $columnField) {
$currentColumn++;
$metadata = $gridField->getColumnMetadata($columnField);
$title = $metadata['title'];
if(isset($this->fieldSorting[$columnField]) && $this->fieldSorting[$columnField]) {
$columnField = $this->fieldSorting[$columnField];
}

View File

@ -1,8 +1,8 @@
<?php
/**
* This class is a snapshot of the current status of a gridfield.
*
* It's main use is to be inserted into a Form as a HiddenField
* This class is a snapshot of the current status of a {@link GridField}. It's
* designed to be inserted into a Form as a HiddenField and passed through to
* actions such as the {@link GridField_FormAction}
*
* @see GridField
*
@ -11,24 +11,16 @@
*/
class GridState extends HiddenField {
/** @var GridField */
/**
* @var GridField
*/
protected $grid;
protected $gridStateData = null;
/**
*
* @param type $d
* @return type
* @var GridState_Data
*/
public static function array_to_object($d) {
if(is_array($d)) {
return (object) array_map(array('GridState', 'array_to_object'), $d);
} else {
return $d;
}
}
protected $data = null;
/**
*
* @param GridField $name
@ -41,42 +33,65 @@ class GridState extends HiddenField {
parent::__construct($grid->getName() . '[GridState]');
}
/**
*
* @param type $value
* @param mixed $d
* @return object
*/
public function setValue($value) {
if (is_string($value)) {
$this->gridStateData = new GridState_Data(json_decode($value, true));
public static function array_to_object($d) {
if(is_array($d)) {
return (object) array_map(array('GridState', 'array_to_object'), $d);
}
parent::setValue($value);
}
public function getData() {
if(!$this->gridStateData) $this->gridStateData = new GridState_Data;
return $this->gridStateData;
return $d;
}
/**
*
* @return type
* @param mixed $value
*/
public function setValue($value) {
if (is_string($value)) {
$this->data = new GridState_Data(json_decode($value, true));
}
parent::setValue($value);
}
/**
* @var GridState_Data
*/
public function getData() {
if(!$this->data) {
$this->data = new GridState_Data();
}
return $this->data;
}
/**
* @return DataList
*/
public function getList() {
return $this->grid->getList();
}
/** @return string */
/**
* Returns a json encoded string representation of this state.
*
* @return string
*/
public function Value() {
if(!$this->gridStateData) {
if(!$this->data) {
return json_encode(array());
}
return json_encode($this->gridStateData->toArray());
return json_encode($this->data->toArray());
}
/**
* Returns a json encoded string representation of this state.
*
* @return type
* @return string
*/
public function dataValue() {
return $this->Value();
@ -100,9 +115,18 @@ class GridState extends HiddenField {
}
/**
* Simple set of data, similar to stdClass, but without the notice-level errors
* Simple set of data, similar to stdClass, but without the notice-level errors.
*
* @see GridState
*
* @package framework
* @subpackage fields-relational
*/
class GridState_Data {
/**
* @var array
*/
protected $data;
public function __construct($data = array()) {
@ -110,42 +134,53 @@ class GridState_Data {
}
public function __get($name) {
if(!isset($this->data[$name])) $this->data[$name] = new GridState_Data;
if(is_array($this->data[$name])) $this->data[$name] = new GridState_Data($this->data[$name]);
if(!isset($this->data[$name])) {
$this->data[$name] = new GridState_Data();
} else if(is_array($this->data[$name])) {
$this->data[$name] = new GridState_Data($this->data[$name]);
}
return $this->data[$name];
}
public function __set($name, $value) {
$this->data[$name] = $value;
}
public function __isset($name) {
return isset($this->data[$name]);
}
public function __toString() {
if(!$this->data) return "";
else return json_encode($this->toArray());
if(!$this->data) {
return "";
}
return json_encode($this->toArray());
}
public function toArray() {
$output = array();
foreach($this->data as $k => $v) {
$output[$k] = (is_object($v) && method_exists($v, 'toArray')) ? $v->toArray() : $v;
}
return $output;
}
}
/**
* @see GridState
*
* @package framework
* @subpackage fields-relational
*/
class GridState_Component implements GridField_HTMLProvider {
public function getHTMLFragments($gridField) {
$forTemplate = new ArrayData(array());
$forTemplate->Fields = new ArrayList;
return array(
'before' => $gridField->getState(false)->Field()
);
}
}

View File

@ -157,16 +157,24 @@
},
onclick: function(e){
var btn = this.closest(':button'), grid = this.getGridField(),
form = this.closest('form'), data = form.find(':input').serialize();
form = this.closest('form'), data = form.find(':input.gridstate').serialize();;
// Add current button
data += '&' + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
// Include any GET parameters from the current URL, as the view state might depend on it.
// For example, a list prefiltered through external search criteria might be passed to GridField.
if(window.location.search) data = window.location.search.replace(/^\?/, '') + '&' + data;
// Include any GET parameters from the current URL, as the view
// state might depend on it.
// For example, a list prefiltered through external search criteria
// might be passed to GridField.
if(window.location.search) {
data = window.location.search.replace(/^\?/, '') + '&' + data;
}
var url = $.path.makeUrlAbsolute(
grid.data('url') + '?' + data,
$('base').attr('href')
);
var url = $.path.makeUrlAbsolute(grid.data('url') + '?' + data, $('base').attr('href'));
var newWindow = window.open(url);
return false;
@ -188,22 +196,30 @@
/**
* Prevents actions from causing an ajax reload of the field.
* Useful e.g. for actions which rely on HTTP response headers being interpreted nativel
* by the browser, like file download triggers.
*
* Useful e.g. for actions which rely on HTTP response headers being
* interpreted natively by the browser, like file download triggers.
*/
$('.ss-gridfield .action.no-ajax').entwine({
onclick: function(e){
var self = this, btn = this.closest(':button'), grid = this.getGridField(),
form = this.closest('form'), data = form.find(':input').serialize();
form = this.closest('form'), data = form.find(':input.gridstate').serialize();
// Add current button
data += '&' + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
// Include any GET parameters from the current URL, as the view state might depend on it.
// For example, a list prefiltered through external search criteria might be passed to GridField.
if(window.location.search) data = window.location.search.replace(/^\?/, '') + '&' + data;
// Include any GET parameters from the current URL, as the view
// state might depend on it. For example, a list pre-filtered
// through external search criteria might be passed to GridField.
if(window.location.search) {
data = window.location.search.replace(/^\?/, '') + '&' + data;
}
window.location.href = $.path.makeUrlAbsolute(
grid.data('url') + '?' + data,
$('base').attr('href')
);
window.location.href = $.path.makeUrlAbsolute(grid.data('url') + '?' + data, $('base').attr('href'));
return false;
}
});
@ -340,7 +356,5 @@
}
}
});
});
}(jQuery));
}(jQuery));

View File

@ -382,9 +382,11 @@ $gf_grid_x: 16px;
background: rgba(#000, 0.7);
padding: 5px;
border-top: $gf_colour_text_shadow;
input {
height:28px; //height of input field - to match design.
}
button.ss-ui-button {
padding: .3em;
line-height: 1;
@ -392,7 +394,11 @@ $gf_grid_x: 16px;
position: relative;
border-bottom-width: 0;
@include border-radius(2px, 2px);
}
}
select {
margin: 0;
}
}
&.first {
@include border-top-left-radius($gf_border_radius);

View File

@ -10,9 +10,15 @@
<tr><% loop Header %><th>$CellString</th><% end_loop %></tr>
</thead>
<tbody>
<% if ItemRows %>
<% loop ItemRows %>
<tr><% loop ItemRow %><td>$CellString</td><% end_loop %></tr>
<% end_loop %>
<% else %>
<tr>
<td colspan="$Header.Count"><p><% _t('GridField.NoItemsFound', 'No items found') %></p></td>
</tr>
<% end_if %>
</tbody>
</table>
<p>