- Patch #639466 by yched: fixed hook_options_list() and XSS filtering, and added more tests.

merge-requests/26/head
Dries Buytaert 2009-12-14 20:18:56 +00:00
parent 7e6fdd8540
commit 13984a7394
9 changed files with 531 additions and 216 deletions

View File

@ -5,5 +5,5 @@ package = Core - fields
version = VERSION
core = 7.x
files[]=list.module
files[]=list.test
files[]=tests/list.test
required = TRUE

View File

@ -115,7 +115,7 @@ function list_field_settings_form($field, $instance, $has_data) {
'#required' => FALSE,
'#rows' => 10,
'#description' => '<p>' . t('The possible values this field can contain. Enter one value per line, in the format key|label. The key is the value that will be stored in the database, and must be a %type value. The label is optional, and the key will be used as the label if no label is specified.', array('%type' => $field['type'] == 'list_text' ? t('text') : t('numeric'))) . '</p>',
'#element_validate' => array('list_allowed_values_validate'),
'#element_validate' => array('list_allowed_values_setting_validate'),
'#list_field_type' => $field['type'],
'#access' => empty($settings['allowed_values_function']),
);
@ -143,99 +143,11 @@ function list_field_settings_form($field, $instance, $has_data) {
return $form;
}
/**
* Implements hook_field_create_field().
*/
function list_field_create_field($field) {
if (array_key_exists($field['type'], list_field_info())) {
// Clear the static cache of allowed values for $field.
$allowed_values = &drupal_static('list_allowed_values', array());
unset($allowed_values[$field['field_name']]);
}
}
/**
* Implements hook_field_update_field().
*/
function list_field_update_field($field, $prior_field, $has_data) {
if (array_key_exists($field['type'], list_field_info())) {
// Clear the static cache of allowed values for $field.
$allowed_values = &drupal_static('list_allowed_values', array());
unset($allowed_values[$field['field_name']]);
}
}
/**
* Create an array of allowed values for this field.
*/
function list_allowed_values($field) {
// This static cache must be cleared whenever $field['field_name']
// changes. This includes when it is created because a different
// field with the same name may have previously existed, as well
// as when it is updated.
$allowed_values = &drupal_static(__FUNCTION__, array());
if (isset($allowed_values[$field['field_name']])) {
return $allowed_values[$field['field_name']];
}
$allowed_values[$field['field_name']] = array();
$function = $field['settings']['allowed_values_function'];
if (!empty($function) && function_exists($function)) {
$allowed_values[$field['field_name']] = $function($field);
}
elseif (!empty($field['settings']['allowed_values'])) {
$allowed_values[$field['field_name']] = list_allowed_values_list($field['settings']['allowed_values'], $field['type'] == 'list');
}
return $allowed_values[$field['field_name']];
}
/**
* Create an array of the allowed values for this field.
*
* Explode a string with keys and labels separated with '|' and with each new
* value on its own line.
*
* @param $string_values
* The list of choices as a string.
* @param $position_keys
* Boolean value indicating whether to generate keys based on the position of
* the value if a key is not manually specified, effectively generating
* integer-based keys. This should only be TRUE for fields that have a type of
* "list". Otherwise the value will be used as the key if not specified.
*/
function list_allowed_values_list($string_values, $position_keys = FALSE) {
$allowed_values = array();
$list = explode("\n", $string_values);
$list = array_map('trim', $list);
$list = array_filter($list, 'strlen');
foreach ($list as $key => $value) {
// Sanitize the user input with a permissive filter.
$value = field_filter_xss($value);
// Check for a manually specified key.
if (strpos($value, '|') !== FALSE) {
list($key, $value) = explode('|', $value);
}
// Otherwise see if we need to use the value as the key. The "list" type
// will automatically convert non-keyed lines to integers.
elseif (!$position_keys) {
$key = $value;
}
$allowed_values[$key] = (isset($value) && $value !== '') ? $value : $key;
}
return $allowed_values;
}
/**
* Element validate callback; check that the entered values are valid.
*/
function list_allowed_values_validate($element, &$form_state) {
$values = list_allowed_values_list($element['#value'], $element['#list_field_type'] == 'list');
function list_allowed_values_setting_validate($element, &$form_state) {
$values = list_extract_allowed_values($element['#value'], $element['#list_field_type'] == 'list');
$field_type = $element['#list_field_type'];
// Check that keys are valid for the field type.
@ -264,6 +176,86 @@ function list_allowed_values_validate($element, &$form_state) {
}
}
/**
* Implements hook_field_update_field().
*/
function list_field_update_field($field, $prior_field, $has_data) {
drupal_static_reset('list_allowed_values');
}
/**
* Returns the set of allowed values for a list field.
*
* The strings are not safe for output. Keys and values of the array should be
* sanitized through field_filter_xss() before being displayed.
*
* @param $field
* The field definition.
*
* @return
* The array of allowed values. Keys of the array are the raw stored values
* (integer or text), values of the array are the display aliases.
*/
function list_allowed_values($field) {
$allowed_values = &drupal_static(__FUNCTION__, array());
if (!isset($allowed_values[$field['id']])) {
$values = array();
$function = $field['settings']['allowed_values_function'];
if (!empty($function) && function_exists($function)) {
$values = $function($field);
}
elseif (!empty($field['settings']['allowed_values'])) {
$position_keys = $field['type'] == 'list';
$values = list_extract_allowed_values($field['settings']['allowed_values'], $position_keys);
}
$allowed_values[$field['id']] = $values;
}
return $allowed_values[$field['id']];
}
/**
* Generates an array of values from a string.
*
* Explode a string with keys and labels separated with '|' and with each new
* value on its own line.
*
* @param $string_values
* The list of choices as a string, in the format expected by the
* 'allowed_values' setting:
* - Values are separated by a carriage return.
* - Each value is in the format "value|label" or "value".
* @param $position_keys
* Boolean value indicating whether to generate keys based on the position of
* the value if a key is not manually specified, effectively generating
* integer-based keys. This should only be TRUE for fields that have a type of
* "list". Otherwise the value will be used as the key if not specified.
*/
function list_extract_allowed_values($string_values, $position_keys = FALSE) {
$values = array();
$list = explode("\n", $string_values);
$list = array_map('trim', $list);
$list = array_filter($list, 'strlen');
foreach ($list as $key => $value) {
// Check for a manually specified key.
if (strpos($value, '|') !== FALSE) {
list($key, $value) = explode('|', $value);
}
// Otherwise see if we need to use the value as the key. The "list" type
// will automatically convert non-keyed lines to integers.
elseif (!$position_keys) {
$key = $value;
}
$values[$key] = (isset($value) && $value !== '') ? $value : $key;
}
return $values;
}
/**
* Implements hook_field_validate().
*
@ -294,6 +286,33 @@ function list_field_is_empty($item, $field) {
return FALSE;
}
/**
* Implements hook_field_widget_info_alter().
*
* The List module does not implement widgets of its own, but reuses the
* widgets defined in options.module.
*
* @see list_options_list().
*/
function list_field_widget_info_alter(&$info) {
$widgets = array(
'options_select' => array('list', 'list_text', 'list_number', 'list_boolean'),
'options_buttons' => array('list', 'list_text', 'list_number', 'list_boolean'),
'options_onoff' => array('list_boolean'),
);
foreach ($widgets as $widget => $field_types) {
$info[$widget]['field types'] = array_merge($info[$widget]['field types'], $field_types);
}
}
/**
* Implements hook_options_list().
*/
function list_options_list($field) {
return list_allowed_values($field);
}
/**
* Implements hook_field_formatter_info().
*/
@ -321,11 +340,11 @@ function list_field_formatter($object_type, $object, $field, $instance, $langcod
$allowed_values = list_allowed_values($field);
foreach ($items as $delta => $item) {
if (isset($allowed_values[$item['value']])) {
$output = $allowed_values[$item['value']];
$output = field_filter_xss($allowed_values[$item['value']]);
}
else {
// If no match was found in allowed values, fall back to the key.
$output = $value;
$output = field_filter_xss($value);
}
$element[$delta] = array('#markup' => $output);
}
@ -333,7 +352,7 @@ function list_field_formatter($object_type, $object, $field, $instance, $langcod
case 'list_key':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => $item['value']);
$element[$delta] = array('#markup' => field_filter_xss($item['value']));
}
break;
}

View File

@ -1,105 +1,133 @@
<?php
// $Id$
class ListFieldTestCase extends DrupalWebTestCase {
/**
* @file
* Tests for the 'List' field types.
*/
/**
* Tests for the 'List' field types.
*/
class ListFieldTestCase extends FieldTestCase {
public static function getInfo() {
return array(
'name' => 'List field',
'description' => "Test the List field type.",
'group' => 'Field types'
'name' => 'List field',
'description' => 'Test the List field type.',
'group' => 'Field types',
);
}
function setUp() {
parent::setUp('field_test');
$this->card_1 = array(
'field_name' => 'card_1',
$this->field_name = 'field_test';
$this->field = array(
'field_name' => $this->field_name,
'type' => 'list',
'cardinality' => 1,
'settings' => array(
'allowed_values' => "1|One\n2|Two\n3|Three\n",
),
);
$this->card_1 = field_create_field($this->card_1);
$this->field = field_create_field($this->field);
$this->instance_1 = array(
'field_name' => $this->card_1['field_name'],
$this->instance = array(
'field_name' => $this->field_name,
'object_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_buttons',
),
);
$this->instance_1 = field_create_instance($this->instance_1);
$this->instance = field_create_instance($this->instance);
}
/**
* Test that allowed values can be updated and that the updates are
* reflected in generated forms.
* Test that allowed values can be updated.
*/
function testUpdateAllowedValues() {
$langcode = LANGUAGE_NONE;
// All three options appear.
$entity = field_test_create_stub_entity();
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][1]), t('Option 1 exists'));
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][2]), t('Option 2 exists'));
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][3]), t('Option 3 exists'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][1]), t('Option 1 exists'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][2]), t('Option 2 exists'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][3]), t('Option 3 exists'));
// Removed options do not appear.
$this->card_1['settings']['allowed_values'] = "2|Two";
field_update_field($this->card_1);
$this->field['settings']['allowed_values'] = "2|Two";
field_update_field($this->field);
$entity = field_test_create_stub_entity();
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(empty($form['card_1'][LANGUAGE_NONE][1]), t('Option 1 does not exist'));
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][2]), t('Option 2 exists'));
$this->assertTrue(empty($form['card_1'][LANGUAGE_NONE][3]), t('Option 3 does not exist'));
$this->assertTrue(empty($form[$this->field_name][$langcode][1]), t('Option 1 does not exist'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][2]), t('Option 2 exists'));
$this->assertTrue(empty($form[$this->field_name][$langcode][3]), t('Option 3 does not exist'));
// Completely new options appear.
$this->card_1['settings']['allowed_values'] = "10|Update\n20|Twenty";
field_update_field($this->card_1);
$this->field['settings']['allowed_values'] = "10|Update\n20|Twenty";
field_update_field($this->field);
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(empty($form['card_1'][LANGUAGE_NONE][1]), t('Option 1 does not exist'));
$this->assertTrue(empty($form['card_1'][LANGUAGE_NONE][2]), t('Option 2 does not exist'));
$this->assertTrue(empty($form['card_1'][LANGUAGE_NONE][3]), t('Option 3 does not exist'));
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][10]), t('Option 10 exists'));
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][20]), t('Option 20 exists'));
$this->assertTrue(empty($form[$this->field_name][$langcode][1]), t('Option 1 does not exist'));
$this->assertTrue(empty($form[$this->field_name][$langcode][2]), t('Option 2 does not exist'));
$this->assertTrue(empty($form[$this->field_name][$langcode][3]), t('Option 3 does not exist'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][10]), t('Option 10 exists'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][20]), t('Option 20 exists'));
// Options are reset when a new field with the same name is created.
field_delete_field($this->card_1['field_name']);
unset($this->card_1['id']);
$this->card_1['settings']['allowed_values'] = "1|One\n2|Two\n3|Three\n";
$this->card_1 = field_create_field($this->card_1);
$this->instance_1 = array(
'field_name' => $this->card_1['field_name'],
field_delete_field($this->field_name);
unset($this->field['id']);
$this->field['settings']['allowed_values'] = "1|One\n2|Two\n3|Three\n";
$this->field = field_create_field($this->field);
$this->instance = array(
'field_name' => $this->field_name,
'object_type' => 'test_entity',
'bundle' => 'test_bundle',
'widget' => array(
'type' => 'options_buttons',
),
);
$this->instance_1 = field_create_instance($this->instance_1);
$this->instance = field_create_instance($this->instance);
$entity = field_test_create_stub_entity();
$form = drupal_get_form('field_test_entity_form', $entity);
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][1]), t('Option 1 exists'));
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][2]), t('Option 2 exists'));
$this->assertTrue(!empty($form['card_1'][LANGUAGE_NONE][3]), t('Option 3 exists'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][1]), t('Option 1 exists'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][2]), t('Option 2 exists'));
$this->assertTrue(!empty($form[$this->field_name][$langcode][3]), t('Option 3 exists'));
}
}
/**
* List module UI tests.
*/
class ListFieldUITestCase extends FieldUITestCase {
class ListFieldUITestCase extends FieldTestCase {
public static function getInfo() {
return array(
'name' => 'List field UI tests',
'name' => 'List field UI',
'description' => 'Test the List field UI functionality.',
'group' => 'Field types',
);
}
function setUp() {
parent::setUp('field_test', 'field_ui');
// Create test user.
$admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy'));
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = strtolower($this->randomName(8)) . '_' .'test';
$type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name));
$this->type = $type->type;
// Store a valid URL name, with hyphens instead of underscores.
$this->hyphen_type = str_replace('_', '-', $this->type);
// Create random field name.
$this->field_label = $this->randomName(8);
$this->field_name = 'field_' . strtolower($this->randomName(8));
}
/**
* Tests that allowed values are properly validated in the UI.
*/
@ -126,23 +154,23 @@ class ListFieldUITestCase extends FieldUITestCase {
$edit = array($element_name => "1|one\n" . $this->randomName(255) . "|two");
$this->drupalPost($admin_path, $edit, t('Save settings'));
$this->assertText("each key must be a string less than 255 characters", t('Form vaildation failed.'));
// Test 'List (boolean)' field type.
$admin_path = $this->createListFieldAndEdit('list_boolean');
$admin_path = $this->createListFieldAndEdit('list_boolean');
// Check that invalid option keys are rejected.
$edit = array($element_name => "1|one\n2|two");
$this->drupalPost($admin_path, $edit, t('Save settings'));
$this->assertText("keys must be either 0 or 1", t('Form vaildation failed.'));
//Check that missing option causes failure.
$edit = array($element_name => "1|one");
$this->drupalPost($admin_path, $edit, t('Save settings'));
$this->assertText("two values are required", t('Form vaildation failed.'));
$this->assertText("two values are required", t('Form vaildation failed.'));
}
/**
* Helper function to create list field of a given type and get the edit page.
*
*
* @param string $type
* 'list', 'list_boolean', 'list_number', or 'list_text'
*/
@ -164,6 +192,6 @@ class ListFieldUITestCase extends FieldUITestCase {
$admin_path = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields/' . $field_name;
return $admin_path;
}
}

View File

@ -0,0 +1,8 @@
;$Id$
name = "List test"
description = "Support module for the List module tests."
core = 7.x
package = Testing
files[] = list_test.module
version = VERSION
hidden = TRUE

View File

@ -0,0 +1,24 @@
<?php
// $Id$
/**
* @file
* Helper module for the List module tests.
*/
/**
* Allowed values callback.
*/
function list_test_allowed_values_callback($field) {
$values = array(
'Group 1' => array(
0 => 'Zero',
),
1 => 'One',
'Group 2' => array(
2 => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>',
),
);
return $values;
}

View File

@ -0,0 +1,65 @@
<?php
// $Id$
/**
* @file
* Hooks provided by the Options module.
*/
/**
* Returns the list of options to be displayed for a field.
*
* Field types willing to enable one or several of the widgets defined in
* options.module (select, radios/checkboxes, on/off checkbox) need to
* implement this hook to specify the list of options to display in the
* widgets.
*
* @param $field
* The field definition.
*
* @return
* The array of options for the field. Array keys are the values to be
* stored, and should be of the data type (string, number...) expected by
* the first 'column' for the field type. Array values are the labels to
* display within the widgets. The labels should NOT be sanitized,
* options.module takes care of sanitation according to the needs of each
* widget. The HTML tags defined in _field_filter_xss_allowed_tags() are
* allowed, other tags will be filtered.
*/
function hook_options_list($field) {
// Sample structure.
$options = array(
0 => t('Zero'),
1 => t('One'),
2 => t('Two'),
3 => t('Three'),
);
// Sample structure with groups. Only one level of nesting is allowed. This
// is only supported by the 'options_select' widget. Other widgets will
// flatten the array.
$options = array(
t('First group') => array(
0 => t('Zero'),
),
t('Second group') => array(
1 => t('One'),
2 => t('Two'),
),
3 => t('Three'),
);
// In actual implementations, the array of options will most probably depend
// on properties of the field. Example from taxonomy.module:
$options = array();
foreach ($field['settings']['allowed_values'] as $tree) {
$terms = taxonomy_get_tree($tree['vid'], $tree['parent']);
if ($terms) {
foreach ($terms as $term) {
$options[$term->tid] = str_repeat('-', $term->depth) . $term->name;
}
}
}
return $options;
}

View File

@ -32,26 +32,32 @@ function options_theme() {
/**
* Implements hook_field_widget_info().
*
* Field type modules willing to use those widgets should:
* - Use hook_field_widget_info_alter() to append their field own types to the
* list of types supported by the widgets,
* - Implement hook_options_list() to provide the list of options.
* See list.module.
*/
function options_field_widget_info() {
return array(
'options_select' => array(
'label' => t('Select list'),
'field types' => array('list', 'list_boolean', 'list_text', 'list_number'),
'field types' => array(),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
),
),
'options_buttons' => array(
'label' => t('Check boxes/radio buttons'),
'field types' => array('list', 'list_boolean', 'list_text', 'list_number'),
'field types' => array(),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
),
),
'options_onoff' => array(
'label' => t('Single on/off checkbox'),
'field types' => array('list_boolean'),
'field types' => array(),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_CUSTOM,
),
@ -66,61 +72,64 @@ function options_field_widget(&$form, &$form_state, $field, $instance, $langcode
// Abstract over the actual field columns, to allow different field types to
// reuse those widgets.
$value_key = key($field['columns']);
$multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
// Form API 'checkboxes' do not suport 0 as an option, so we replace it with
// a placeholder within the form workflow.
$zero_placeholder = $instance['widget']['type'] == 'options_buttons' && $multiple;
// Collect available options for the field.
$options = options_get_options($field, $instance, $zero_placeholder);
// Put current field values in shape.
$default_value = _options_storage_to_form($items, $options, $value_key, $zero_placeholder);
switch ($instance['widget']['type']) {
case 'options_select':
$type = str_replace('options_', '', $instance['widget']['type']);
$multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
$required = $element['#required'];
$properties = _options_properties($type, $multiple, $required);
// Prepare the list of options.
$options = _options_get_options($field, $instance, $properties);
// Put current field values in shape.
$default_value = _options_storage_to_form($items, $options, $value_key, $properties);
switch ($type) {
case 'select':
$element += array(
'#type' => 'select',
'#default_value' => $default_value,
// Do not display a 'multiple' select box if there is only one option.
'#multiple' => $multiple && count($options) > 1,
'#options' => $options,
'#value_key' => $value_key,
'#element_validate' => array('options_field_widget_validate'),
);
break;
case 'options_buttons':
$type = $multiple ? 'checkboxes' : 'radios';
case 'buttons':
// If required and there is one single option, preselect it.
if ($element['#required'] && count($options) == 1) {
if ($required && count($options) == 1) {
reset($options);
$default_value = array(key($options));
}
$element += array(
'#type' => $type,
'#type' => $multiple ? 'checkboxes' : 'radios',
// Radio buttons need a scalar value.
'#default_value' => ($type == 'radios') ? reset($default_value) : $default_value,
'#default_value' => $multiple ? $default_value : reset($default_value),
'#options' => $options,
'#zero_placeholder' => $zero_placeholder,
'#value_key' => $value_key,
'#element_validate' => array('options_field_widget_validate'),
);
break;
case 'options_onoff':
case 'onoff':
$keys = array_keys($options);
$off_value = (!empty($keys) && isset($keys[0])) ? $keys[0] : NULL;
$on_value = (!empty($keys) && isset($keys[1])) ? $keys[1] : NULL;
$off_value = array_shift($keys);
$on_value = array_shift($keys);
$element += array(
'#type' => 'checkbox',
'#title' => isset($options[$on_value]) ? $options[$on_value] : '',
'#default_value' => (isset($default_value[0]) && $default_value[0] == $on_value) ? 1 : 0,
'#on_value' => $on_value,
'#off_value' => $off_value,
'#value_key' => $value_key,
'#element_validate' => array('options_field_widget_validate'),
);
// Override the title from the incoming $element.
$element['#title'] = isset($options[$on_value]) ? $options[$on_value] : '';
break;
}
$element += array(
'#value_key' => $value_key,
'#element_validate' => array('options_field_widget_validate'),
'#properties' => $properties,
);
return $element;
}
@ -135,54 +144,123 @@ function options_field_widget_validate($element, &$form_state) {
}
/**
* Prepares the options for a field.
* Describes the preparation steps required by each widget.
*/
function options_get_options($field, $instance, $zero_placeholder) {
// Check if there is a module hook for the option values, otherwise try
// list_allowed_values() for an options list.
// @todo This should be turned into a hook_options_allowed_values(), exposed
// by options.module.
$function = $field['module'] . '_allowed_values';
$options = function_exists($function) ? $function($field) : (array) list_allowed_values($field);
function _options_properties($type, $multiple, $required) {
$base = array(
'zero_placeholder' => FALSE,
'filter_xss' => FALSE,
'strip_tags' => FALSE,
'empty_value' => FALSE,
'optgroups' => FALSE,
);
switch ($type) {
case 'select':
$properties = array(
// Select boxes do not support any HTML tag.
'strip_tags' => TRUE,
'empty_value' => !$required,
'optgroups' => TRUE,
);
break;
case 'buttons':
$properties = array(
'filter_xss' => TRUE,
// Form API 'checkboxes' do not suport 0 as an option, so we replace it with
// a placeholder within the form workflow.
'zero_placeholder' => $multiple,
// Checkboxes do not need a 'none' choice.
'empty_value' => !$required && !$multiple,
);
break;
case 'onoff':
$properties = array(
'filter_xss' => TRUE,
);
break;
}
return $properties + $base;
}
/**
* Collects the options for a field.
*/
function _options_get_options($field, $instance, $properties) {
// Get the list of options.
$options = (array) module_invoke($field['module'], 'options_list', $field);
// Sanitize the options.
_options_prepare_options($options, $properties);
if (!$properties['optgroups']) {
$options = options_array_flatten($options);
}
if ($properties['empty_value']) {
$options = array('_none' => theme('options_none', array('instance' => $instance))) + $options;
}
return $options;
}
/**
* Sanitizes the options.
*
* The function is recursive to support optgroups.
*/
function _options_prepare_options(&$options, $properties) {
// Substitute the '_0' placeholder.
if ($zero_placeholder) {
if ($properties['zero_placeholder']) {
$values = array_keys($options);
$labels = array_values($options);
// Use a strict comparison, because 0 == 'any string'.
$index = array_search(0, $values, TRUE);
if ($index !== FALSE) {
if ($index !== FALSE && !is_array($options[$index])) {
$values[$index] = '_0';
$options = array_combine($values, array_values($options));
$options = array_combine($values, $labels);
}
}
// Add an empty choice for
// - non required radios
// - non required selects
if (!$instance['required']) {
if (($instance['widget']['type'] == 'options_buttons' && ($field['cardinality'] == 1)) || ($instance['widget']['type'] == 'options_select')) {
$options = array('_none' => theme('options_none', array('instance' => $instance))) + $options;
foreach ($options as $value => $label) {
// Recurse for optgroups.
if (is_array($label)) {
_options_prepare_options($options[$value], $properties);
}
else {
if ($properties['strip_tags']) {
$options[$value] = strip_tags($label);
}
if ($properties['filter_xss']) {
$options[$value] = field_filter_xss($label);
}
}
}
return $options;
}
/**
* Transforms stored field values into the format the widgets need.
*/
function _options_storage_to_form($items, $options, $column, $zero_placeholder) {
function _options_storage_to_form($items, $options, $column, $properties) {
$items_transposed = options_array_transpose($items);
$values = (isset($items_transposed[$column]) && is_array($items_transposed[$column])) ? $items_transposed[$column] : array();
// Substitute the '_0' placeholder.
if ($zero_placeholder) {
if ($properties['zero_placeholder']) {
$index = array_search('0', $values);
if ($index !== FALSE) {
$values[$index] = '_0';
}
}
// Discard values that are not in the current list of options.
// Discard values that are not in the current list of options. Flatten the
// array if needed.
if ($properties['optgroups']) {
$options = options_array_flatten($options);
}
$values = array_values(array_intersect($values, array_keys($options)));
return $values;
}
@ -192,6 +270,7 @@ function _options_storage_to_form($items, $options, $column, $zero_placeholder)
*/
function _options_form_to_storage($element) {
$values = array_values((array) $element['#value']);
$properties = $element['#properties'];
// On/off checkbox: transform '0 / 1' into the 'on / off' values.
if ($element['#type'] == 'checkbox') {
@ -199,7 +278,7 @@ function _options_form_to_storage($element) {
}
// Substitute the '_0' placeholder.
if (!empty($element['#zero_placeholder'])) {
if ($properties['zero_placeholder']) {
$index = array_search('_0', $values);
if ($index !== FALSE) {
$values[$index] = 0;
@ -208,9 +287,11 @@ function _options_form_to_storage($element) {
// Filter out the 'none' option. Use a strict comparison, because
// 0 == 'any string'.
$index = array_search('_none', $values, TRUE);
if ($index !== FALSE) {
unset($values[$index]);
if ($properties['empty_value']) {
$index = array_search('_none', $values, TRUE);
if ($index !== FALSE) {
unset($values[$index]);
}
}
// Make sure we populate at least an empty value.
@ -252,6 +333,29 @@ function options_array_transpose($array) {
return $result;
}
/**
* Flattens an array of allowed values.
*
* @param $array
* A single or multidimensional array.
* @return
* A flattened array.
*/
function options_array_flatten($array) {
$result = array();
if (is_array($array)) {
foreach ($array as $key => $value) {
if (is_array($value)) {
$result += options_array_flatten($value);
}
else {
$result[$key] = $value;
}
}
}
return $result;
}
/**
* Implements hook_field_widget_error().
*/

View File

@ -11,7 +11,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
}
function setUp() {
parent::setUp('field_test');
parent::setUp('field_test', 'list_test');
// Field with cardinality 1.
$this->card_1 = array(
@ -20,7 +20,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
'cardinality' => 1,
'settings' => array(
// Make sure that 0 works as an option.
'allowed_values' => "0|Zero\n1|One\n2|Two\n",
'allowed_values' => "0|Zero\n1|One\n2|Some <script>dangerous</script> & unescaped <strong>markup</strong>\n",
),
);
$this->card_1 = field_create_field($this->card_1);
@ -32,7 +32,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
'cardinality' => 2,
'settings' => array(
// Make sure that 0 works as an option.
'allowed_values' => "0|Zero\n1|One\n2|Two\n",
'allowed_values' => "0|Zero\n1|One\n2|Some <script>dangerous</script> & unescaped <strong>markup</strong>\n",
),
);
$this->card_2 = field_create_field($this->card_2);
@ -44,7 +44,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
'cardinality' => 1,
'settings' => array(
// Make sure that 0 works as a 'on' value'.
'allowed_values' => "1|No\n0|Yes\n",
'allowed_values' => "1|No\n0|Some <script>dangerous</script> & unescaped <strong>markup</strong>\n",
),
);
$this->bool = field_create_field($this->bool);
@ -81,6 +81,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
$this->assertNoFieldChecked("edit-card-1-$langcode-0");
$this->assertNoFieldChecked("edit-card-1-$langcode-1");
$this->assertNoFieldChecked("edit-card-1-$langcode-2");
$this->assertRaw('Some dangerous &amp; unescaped <strong>markup</strong>', t('Option text was properly filtered.'));
// Select first option.
$edit = array("card_1[$langcode]" => 0);
@ -98,7 +99,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array());
// Required radios with one option is auto-selected.
// Check that required radios with one option is auto-selected.
$this->card_1['settings']['allowed_values'] = '99|Only allowed value';
field_update_field($this->card_1);
$instance['required'] = TRUE;
@ -137,6 +138,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
$this->assertNoFieldChecked("edit-card-2-$langcode--0");
$this->assertNoFieldChecked("edit-card-2-$langcode-1");
$this->assertNoFieldChecked("edit-card-2-$langcode-2");
$this->assertRaw('Some dangerous &amp; unescaped <strong>markup</strong>', t('Option text was properly filtered.'));
// Submit form: select first and third options.
$edit = array(
@ -223,6 +225,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
$this->assertNoOptionSelected("edit-card-1-$langcode", 0);
$this->assertNoOptionSelected("edit-card-1-$langcode", 1);
$this->assertNoOptionSelected("edit-card-1-$langcode", 2);
$this->assertRaw('Some dangerous &amp; unescaped markup', t('Option text was properly filtered.'));
// Submit form: select first option.
$edit = array("card_1[$langcode]" => 0);
@ -248,6 +251,38 @@ class OptionsWidgetsTestCase extends FieldTestCase {
// We do not have to test that a required select list with one option is
// auto-selected because the browser does it for us.
// Test optgroups.
$this->card_1['settings']['allowed_values'] = NULL;
$this->card_1['settings']['allowed_values_function'] = 'list_test_allowed_values_callback';
field_update_field($this->card_1);
$instance['required'] = FALSE;
field_update_instance($instance);
// Display form: with no field data, nothing is selected
$this->drupalGet('test-entity/' . $entity->ftid .'/edit');
$this->assertNoOptionSelected("edit-card-1-$langcode", 0);
$this->assertNoOptionSelected("edit-card-1-$langcode", 1);
$this->assertNoOptionSelected("edit-card-1-$langcode", 2);
$this->assertRaw('Some dangerous &amp; unescaped markup', t('Option text was properly filtered.'));
$this->assertRaw('Group 1', t('Option groups are displayed.'));
// Submit form: select first option.
$edit = array("card_1[$langcode]" => 0);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/' . $entity->ftid .'/edit');
$this->assertOptionSelected("edit-card-1-$langcode", 0);
$this->assertNoOptionSelected("edit-card-1-$langcode", 1);
$this->assertNoOptionSelected("edit-card-1-$langcode", 2);
// Submit form: Unselect the option.
$edit = array("card_1[$langcode]" => '_none');
$this->drupalPost('test-entity/' . $entity->ftid .'/edit', $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_1', $langcode, array());
}
/**
@ -277,6 +312,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
$this->assertNoOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertNoOptionSelected("edit-card-2-$langcode", 2);
$this->assertRaw('Some dangerous &amp; unescaped markup', t('Option text was properly filtered.'));
// Submit form: select first and third options.
$edit = array("card_2[$langcode][]" => array(0 => 0, 2 => 2));
@ -331,6 +367,39 @@ class OptionsWidgetsTestCase extends FieldTestCase {
// We do not have to test that a required select list with one option is
// auto-selected because the browser does it for us.
// Test optgroups.
// Use a callback function defining optgroups.
$this->card_2['settings']['allowed_values'] = NULL;
$this->card_2['settings']['allowed_values_function'] = 'list_test_allowed_values_callback';
field_update_field($this->card_2);
$instance['required'] = FALSE;
field_update_instance($instance);
// Display form: with no field data, nothing is selected.
$this->drupalGet('test-entity/' . $entity->ftid .'/edit');
$this->assertNoOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertNoOptionSelected("edit-card-2-$langcode", 2);
$this->assertRaw('Some dangerous &amp; unescaped markup', t('Option text was properly filtered.'));
$this->assertRaw('Group 1', t('Option groups are displayed.'));
// Submit form: select first option.
$edit = array("card_2[$langcode][]" => array(0 => 0));
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array(0));
// Display form: check that the right options are selected.
$this->drupalGet('test-entity/' . $entity->ftid .'/edit');
$this->assertOptionSelected("edit-card-2-$langcode", 0);
$this->assertNoOptionSelected("edit-card-2-$langcode", 1);
$this->assertNoOptionSelected("edit-card-2-$langcode", 2);
// Submit form: Unselect the option.
$edit = array("card_2[$langcode][]" => array('_none' => '_none'));
$this->drupalPost('test-entity/' . $entity->ftid .'/edit', $edit, t('Save'));
$this->assertFieldValues($entity_init, 'card_2', $langcode, array());
}
/**
@ -358,6 +427,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
// Display form: with no field data, option is unchecked.
$this->drupalGet('test-entity/' . $entity->ftid .'/edit');
$this->assertNoFieldChecked("edit-bool-$langcode");
$this->assertRaw('Some dangerous &amp; unescaped <strong>markup</strong>', t('Option text was properly filtered.'));
// Submit form: check the option.
$edit = array("bool[$langcode]" => TRUE);

View File

@ -983,16 +983,6 @@ function taxonomy_field_info() {
/**
* Implements hook_field_widget_info().
*
* We need custom handling of multiple values because we need
* to combine them into a options list rather than display
* cardinality elements. We will use the field module's default
* handling for default values.
*
* Callbacks can be omitted if default handing is used.
* They're included here just so this module can be used
* as an example for custom modules that might do things
* differently.
*/
function taxonomy_field_widget_info() {
return array(
@ -1018,6 +1008,13 @@ function taxonomy_field_widget_info_alter(&$info) {
$info['options_buttons']['field types'][] = 'taxonomy_term';
}
/**
* Implements hook_options_list().
*/
function taxonomy_options_list($field) {
return taxonomy_allowed_values($field);
}
/**
* Implements hook_field_schema().
*/
@ -1122,12 +1119,12 @@ function taxonomy_field_formatter($object_type, $object, $field, $instance, $lan
}
/**
* Create an array of the allowed values for this field.
* Returns the set of valid terms for a taxonomy field.
*
* Call the field's allowed_values function to retrieve the allowed
* values array.
*
* @see _taxonomy_term_select()
* @param $field
* The field definition.
* @return
* The array of valid terms for this field, keyed by term id.
*/
function taxonomy_allowed_values($field) {
$options = array();