Commit 13984a73 authored by Dries's avatar Dries
Browse files

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

parent 7e6fdd85
......@@ -5,5 +5,5 @@ package = Core - fields
version = VERSION
core = 7.x
files[]=list.module
files[]=list.test
files[]=tests/list.test
required = TRUE
......@@ -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']),
);
......@@ -144,13 +144,35 @@ function list_field_settings_form($field, $instance, $has_data) {
}
/**
* Implements hook_field_create_field().
* Element validate callback; check that the entered values are valid.
*/
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']]);
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.
foreach ($values as $key => $value) {
if ($field_type == 'list_number' && !is_numeric($key)) {
form_error($element, t('Allowed values list: each key must be a valid integer or decimal.'));
break;
}
elseif ($field_type == 'list_text' && strlen($key) > 255) {
form_error($element, t('Allowed values list: each key must be a string less than 255 characters.'));
break;
}
elseif ($field_type == 'list' && !preg_match('/^-?\d+$/', $key)) {
form_error($element, t('Allowed values list: keys must be integers.'));
break;
}
elseif ($field_type == 'list_boolean' && !in_array($key, array('0', '1'))) {
form_error($element, t('Allowed values list: keys must be either 0 or 1.'));
break;
}
}
// Check that boolean fields get two values.
if ($field_type == 'list_boolean' && count($values) != 2) {
form_error($element, t('Allowed values list: two values are required.'));
}
}
......@@ -158,64 +180,67 @@ function list_field_create_field($field) {
* 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']]);
}
drupal_static_reset('list_allowed_values');
}
/**
* Create an array of allowed values for this field.
* 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) {
// 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']];
}
if (!isset($allowed_values[$field['id']])) {
$values = array();
$allowed_values[$field['field_name']] = 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);
}
$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');
$allowed_values[$field['id']] = $values;
}
return $allowed_values[$field['field_name']];
return $allowed_values[$field['id']];
}
/**
* Create an array of the allowed values for this field.
* 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.
* 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_allowed_values_list($string_values, $position_keys = FALSE) {
$allowed_values = array();
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) {
// 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);
......@@ -225,43 +250,10 @@ function list_allowed_values_list($string_values, $position_keys = FALSE) {
elseif (!$position_keys) {
$key = $value;
}
$allowed_values[$key] = (isset($value) && $value !== '') ? $value : $key;
$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');
$field_type = $element['#list_field_type'];
// Check that keys are valid for the field type.
foreach ($values as $key => $value) {
if ($field_type == 'list_number' && !is_numeric($key)) {
form_error($element, t('Allowed values list: each key must be a valid integer or decimal.'));
break;
}
elseif ($field_type == 'list_text' && strlen($key) > 255) {
form_error($element, t('Allowed values list: each key must be a string less than 255 characters.'));
break;
}
elseif ($field_type == 'list' && !preg_match('/^-?\d+$/', $key)) {
form_error($element, t('Allowed values list: keys must be integers.'));
break;
}
elseif ($field_type == 'list_boolean' && !in_array($key, array('0', '1'))) {
form_error($element, t('Allowed values list: keys must be either 0 or 1.'));
break;
}
}
// Check that boolean fields get two values.
if ($field_type == 'list_boolean' && count($values) != 2) {
form_error($element, t('Allowed values list: two values are required.'));
}
return $values;
}
/**
......@@ -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;
}
......
<?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;
}
}
;$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
<?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;
}
<?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;
}
......@@ -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,