summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/field/modules/list/list.info2
-rw-r--r--modules/field/modules/list/list.module165
-rw-r--r--modules/field/modules/list/tests/list.test (renamed from modules/field/modules/list/list.test)122
-rw-r--r--modules/field/modules/list/tests/list_test.info8
-rw-r--r--modules/field/modules/list/tests/list_test.module24
-rw-r--r--modules/field/modules/options/options.api.php65
-rw-r--r--modules/field/modules/options/options.module208
-rw-r--r--modules/field/modules/options/options.test80
-rw-r--r--modules/taxonomy/taxonomy.module27
9 files changed, 508 insertions, 193 deletions
diff --git a/modules/field/modules/list/list.info b/modules/field/modules/list/list.info
index af5202474..bf4bb8182 100644
--- a/modules/field/modules/list/list.info
+++ b/modules/field/modules/list/list.info
@@ -5,5 +5,5 @@ package = Core - fields
version = VERSION
core = 7.x
files[]=list.module
-files[]=list.test
+files[]=tests/list.test
required = TRUE
diff --git a/modules/field/modules/list/list.module b/modules/field/modules/list/list.module
index 9838f0a72..29e4ea4d2 100644
--- a/modules/field/modules/list/list.module
+++ b/modules/field/modules/list/list.module
@@ -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;
}
/**
@@ -295,6 +287,33 @@ function list_field_is_empty($item, $field) {
}
/**
+ * 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().
*/
function list_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;
}
diff --git a/modules/field/modules/list/list.test b/modules/field/modules/list/tests/list.test
index d53a0fd05..805d3485f 100644
--- a/modules/field/modules/list/list.test
+++ b/modules/field/modules/list/tests/list.test
@@ -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;
}
-
+
}
diff --git a/modules/field/modules/list/tests/list_test.info b/modules/field/modules/list/tests/list_test.info
new file mode 100644
index 000000000..32fec873d
--- /dev/null
+++ b/modules/field/modules/list/tests/list_test.info
@@ -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
diff --git a/modules/field/modules/list/tests/list_test.module b/modules/field/modules/list/tests/list_test.module
new file mode 100644
index 000000000..4fb1998eb
--- /dev/null
+++ b/modules/field/modules/list/tests/list_test.module
@@ -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;
+}
diff --git a/modules/field/modules/options/options.api.php b/modules/field/modules/options/options.api.php
new file mode 100644
index 000000000..72ee8674b
--- /dev/null
+++ b/modules/field/modules/options/options.api.php
@@ -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;
+}
diff --git a/modules/field/modules/options/options.module b/modules/field/modules/options/options.module
index 42f881088..78a32e2ca 100644
--- a/modules/field/modules/options/options.module
+++ b/modules/field/modules/options/options.module
@@ -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']);
+
+ $type = str_replace('options_', '', $instance['widget']['type']);
$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);
+ $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, $zero_placeholder);
+ $default_value = _options_storage_to_form($items, $options, $value_key, $properties);
- switch ($instance['widget']['type']) {
- case 'options_select':
+ 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.
@@ -253,6 +334,29 @@ function options_array_transpose($array) {
}
/**
+ * 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().
*/
function options_field_widget_error($element, $error) {
diff --git a/modules/field/modules/options/options.test b/modules/field/modules/options/options.test
index d8bfeac06..764c45c52 100644
--- a/modules/field/modules/options/options.test
+++ b/modules/field/modules/options/options.test
@@ -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);
diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module
index e21728c9d..f17aa1b4e 100644
--- a/modules/taxonomy/taxonomy.module
+++ b/modules/taxonomy/taxonomy.module
@@ -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(
@@ -1019,6 +1009,13 @@ function taxonomy_field_widget_info_alter(&$info) {
}
/**
+ * Implements hook_options_list().
+ */
+function taxonomy_options_list($field) {
+ return taxonomy_allowed_values($field);
+}
+
+/**
* Implements hook_field_schema().
*/
function taxonomy_field_schema($field) {
@@ -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();