Commit 284f2b11 authored by Dries's avatar Dries

- Patch #902644 by sun, tobiasb: machine names are too hard to implement. Date...

- Patch #902644 by sun, tobiasb: machine names are too hard to implement. Date types and menu names are not validated.

  This patch fixes a bug, but is also a last minute clean-up that will help with better distribution support. We discussed this in http://drupal.org/node/933846.
parent 3e6b9b5e
......@@ -1575,7 +1575,7 @@ function form_error(&$element, $message = '') {
* was clicked when the form was submitted, as well as the sanitized
* $_POST data.
*/
function form_builder($form_id, $element, &$form_state) {
function form_builder($form_id, &$element, &$form_state) {
// Initialize as unprocessed.
$element['#processed'] = FALSE;
......@@ -1602,8 +1602,11 @@ function form_builder($form_id, $element, &$form_state) {
$element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action'];
}
// Store a complete copy of the form in form_state prior to building the form.
$form_state['complete form'] = $element;
// Store a reference to the complete form in $form_state prior to building
// the form. This allows advanced #process and #after_build callbacks to
// perform changes elsewhere in the form.
$form_state['complete form'] = &$element;
// Set a flag if we have a correct form submission. This is always TRUE for
// programmed forms coming from drupal_form_submit(), or if the form_id coming
// from the POST data is set and matches the current form_id.
......@@ -1749,9 +1752,6 @@ function form_builder($form_id, $element, &$form_state) {
// @todo Legacy support. Remove in Drupal 8.
$form_state['clicked_button'] = $form_state['triggering_element'];
}
// Update the copy of the complete form for usage in validation handlers.
$form_state['complete form'] = $element;
}
return $element;
}
......@@ -3033,6 +3033,130 @@ function form_process_tableselect($element) {
return $element;
}
/**
* Processes a machine-readable name form element.
*
* @param $element
* The form element to process. Properties used:
* - #machine_name: An associative array containing:
* - exists: A function name to invoke for checking whether a submitted
* machine name value already exists. The submitted value is passed as
* argument. In most cases, an existing API or menu argument loader
* function can be re-used. The callback is only invoked, if the submitted
* value differs from the element's #default_value.
* - source: (optional) The #array_parents of the form element containing
* the human-readable name (i.e., as contained in the $form structure) to
* use as source for the machine name. Defaults to array('name').
* - label: (optional) A text to display as label for the machine name value
* after the human-readable name form element. Defaults to "Machine name".
* - replace_pattern: (optional) A regular expression (without delimiters)
* matching disallowed characters in the machine name. Defaults to
* '[^a-z0-9_]+'.
* - replace: (optional) A character to replace disallowed characters in the
* machine name via JavaScript. Defaults to '_' (underscore). When using a
* different character, 'replace_pattern' needs to be set accordingly.
* - error: (optional) A custom form error message string to show, if the
* machine name contains disallowed characters.
* - #maxlength: (optional) Should be set to the maximum allowed length of the
* machine name. Defaults to 64.
* - #disabled: (optional) Should be set to TRUE in case an existing machine
* name must not be changed after initial creation.
*/
function form_process_machine_name($element, &$form_state) {
// Apply default form element properties.
$element += array(
'#title' => t('Machine-readable name'),
'#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
'#machine_name' => array(),
);
// A form element that only wants to set one #machine_name property (usually
// 'source' only) would leave all other properties undefined, if the defaults
// were defined in hook_element_info(). Therefore, we apply the defaults here.
$element['#machine_name'] += array(
'source' => array('name'),
'target' => '#' . $element['#id'],
'label' => t('Machine name'),
'replace_pattern' => '[^a-z0-9_]+',
'replace' => '_',
);
// The source element defaults to array('name'), but may have been overidden.
if (empty($element['#machine_name']['source'])) {
return $element;
}
// Retrieve the form element containing the human-readable name from the
// complete form in $form_state. By reference, because we need to append
// a #field_suffix that will hold the live preview.
$key_exists = NULL;
$source = drupal_array_get_nested_value($form_state['complete form'], $element['#machine_name']['source'], $key_exists);
if (!$key_exists) {
return $element;
}
// Append a field suffix to the source form element, which will contain
// the live preview of the machine name.
$suffix_id = $source['#id'] . '-machine-name-suffix';
$source += array('#field_suffix' => '');
$source['#field_suffix'] .= ' <small id="' . $suffix_id . '">&nbsp;</small>';
$parents = array_merge($element['#machine_name']['source'], array('#field_suffix'));
drupal_array_set_nested_value($form_state['complete form'], $parents, $source['#field_suffix']);
$element['#machine_name']['suffix'] = '#' . $suffix_id;
$js_settings = array(
'type' => 'setting',
'data' => array(
'machineName' => array(
'#' . $source['#id'] => $element['#machine_name'],
),
),
);
$element['#attached']['js'][] = 'misc/machine-name.js';
$element['#attached']['js'][] = $js_settings;
return $element;
}
/**
* Form element validation handler for #type 'machine_name'.
*
* Note that #maxlength is validated by _form_validate() already.
*/
function form_validate_machine_name(&$element, &$form_state) {
// Verify that the machine name not only consists of replacement tokens.
if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) {
form_error($element, t('The machine-readable name must contain unique characters.'));
}
// Verify that the machine name contains no disallowed characters.
if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) {
if (!isset($element['#machine_name']['error'])) {
// Since a hyphen is the most common alternative replacement character,
// a corresponding validation error message is supported here.
if ($element['#machine_name']['replace'] == '-') {
form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
}
// Otherwise, we assume the default (underscore).
else {
form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
}
}
else {
form_error($element, $element['#machine_name']['error']);
}
}
// Verify that the machine name is unique.
if ($element['#default_value'] !== $element['#value']) {
$function = $element['#machine_name']['exists'];
if ($function($element['#value'])) {
form_error($element, t('The machine-readable name is already in use. It must be unique.'));
}
}
}
/**
* Adds fieldsets to the specified group or adds group members to this
* fieldset.
......
......@@ -419,57 +419,45 @@ function menu_edit_item_submit($form, &$form_state) {
*/
function menu_edit_menu($form, &$form_state, $type, $menu = array()) {
$system_menus = menu_list_system_menus();
$menu += array('menu_name' => '', 'old_name' => '', 'title' => '', 'description' => '');
if (!empty($menu['menu_name'])) {
$menu['old_name'] = $menu['menu_name'];
}
$form['old_name'] = array('#type' => 'value', '#value' => $menu['old_name']);
$menu += array(
'menu_name' => '',
'old_name' => !empty($menu['menu_name']) ? $menu['menu_name'] : '',
'title' => '',
'description' => '',
);
// Allow menu_edit_menu_submit() and other form submit handlers to determine
// whether the menu already exists.
$form['#insert'] = empty($menu['old_name']);
$form['old_name'] = array(
'#type' => 'value',
'#value' => $menu['old_name'],
);
// The title of a system menu cannot be altered.
if (isset($system_menus[$menu['menu_name']])) {
$form['title'] = array('#type' => 'value', '#value' => $menu['title']);
}
else {
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $menu['title'],
'#required' => TRUE,
'#field_suffix' => ' <small id="edit-title-suffix">&nbsp;</small>',
);
}
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $menu['title'],
'#required' => TRUE,
// The title of a system menu cannot be altered.
'#access' => !isset($system_menus[$menu['menu_name']]),
);
// The internal menu name can only be defined during initial menu creation.
if (!empty($menu['old_name'])) {
$form['#insert'] = FALSE;
$form['menu_name'] = array('#type' => 'value', '#value' => $menu['menu_name']);
}
else {
$form['#insert'] = TRUE;
$js_settings = array(
'type' => 'setting',
'data' => array(
'machineReadableValue' => array(
'title' => array(
'text' => t('URL path'),
'target' => 'menu-name',
'searchPattern' => '[^a-z0-9]+',
'replaceToken' => '-',
),
),
),
);
$form['menu_name'] = array(
'#type' => 'textfield',
'#title' => t('Menu name'),
'#maxsize' => MENU_MAX_MENU_NAME_LENGTH_UI,
'#description' => t('This text will be used to construct the URL for the menu. The name must contain only lowercase letters, numbers and hyphens, and must be unique.'),
'#required' => TRUE,
'#attached' => array(
'js' => array(drupal_get_path('module', 'system') . '/system.js', $js_settings),
),
);
}
$form['menu_name'] = array(
'#type' => 'machine_name',
'#title' => t('Menu name'),
'#default_value' => $menu['menu_name'],
'#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI,
'#description' => t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'),
'#machine_name' => array(
'exists' => 'menu_edit_menu_name_exists',
'source' => array('title'),
'label' => t('URL path'),
'replace_pattern' => '[^a-z0-9-]+',
'replace' => '-',
),
// A menu's machine name cannot be changed.
'#disabled' => !empty($menu['old_name']) || isset($system_menus[$menu['menu_name']]),
);
$form['description'] = array(
'#type' => 'textarea',
......@@ -560,26 +548,18 @@ function menu_delete_menu_confirm_submit($form, &$form_state) {
}
/**
* Validates the human and machine-readable names when adding or editing a menu.
* Returns whether a menu name already exists.
*
* @see menu_edit_menu()
* @see form_validate_machine_name()
*/
function menu_edit_menu_validate($form, &$form_state) {
$item = $form_state['values'];
if (preg_match('/[^a-z0-9-]/', $item['menu_name'])) {
form_set_error('menu_name', t('The menu name may only consist of lowercase letters, numbers, and hyphens.'));
}
if ($form['#insert']) {
if (strlen($item['menu_name']) > MENU_MAX_MENU_NAME_LENGTH_UI) {
form_set_error('menu_name', format_plural(MENU_MAX_MENU_NAME_LENGTH_UI, "The menu name can't be longer than 1 character.", "The menu name can't be longer than @count characters."));
}
function menu_edit_menu_name_exists($value) {
// 'menu-' is added to the menu name to avoid name-space conflicts.
$value = 'menu-' . $value;
$custom_exists = db_query_range('SELECT 1 FROM {menu_custom} WHERE menu_name = :menu', 0, 1, array(':menu' => $value))->fetchField();
$link_exists = db_query_range("SELECT 1 FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $value))->fetchField();
// We will add 'menu-' to the menu name to help avoid name-space conflicts.
$item['menu_name'] = 'menu-' . $item['menu_name'];
$custom_exists = db_query_range('SELECT 1 FROM {menu_custom} WHERE menu_name = :menu', 0, 1, array(':menu' => $item['menu_name']))->fetchField();
$link_exists = db_query_range("SELECT 1 FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $item['menu_name']))->fetchField();
if ($custom_exists || $link_exists) {
form_set_error('menu_name', t('The menu already exists.'));
}
}
return $custom_exists || $link_exists;
}
/**
......
......@@ -140,7 +140,11 @@ class MenuTestCase extends DrupalWebTestCase {
$this->drupalPost('admin/structure/menu/add', $edit, t('Save'));
// Verify that using a menu_name that is too long results in a validation message.
$this->assertText(format_plural(MENU_MAX_MENU_NAME_LENGTH_UI, "The menu name can't be longer than 1 character.", "The menu name can't be longer than @count characters."), t('Validation failed when menu name is too long.'));
$this->assertRaw(t('!name cannot be longer than %max characters but is currently %length characters long.', array(
'!name' => t('Menu name'),
'%max' => MENU_MAX_MENU_NAME_LENGTH_UI,
'%length' => drupal_strlen($menu_name),
)));
// Change the menu_name so it no longer exceeds the maximum length.
$menu_name = substr(hash('sha256', $this->randomName(16)), 0, MENU_MAX_MENU_NAME_LENGTH_UI);
......@@ -148,7 +152,11 @@ class MenuTestCase extends DrupalWebTestCase {
$this->drupalPost('admin/structure/menu/add', $edit, t('Save'));
// Verify that no validation error is given for menu_name length.
$this->assertNoText(format_plural(MENU_MAX_MENU_NAME_LENGTH_UI, "The menu name can't be longer than 1 character.", "The menu name can't be longer than @count characters."), t('Validation failed when menu name is too long.'));
$this->assertNoRaw(t('!name cannot be longer than %max characters but is currently %length characters long.', array(
'!name' => t('Menu name'),
'%max' => MENU_MAX_MENU_NAME_LENGTH_UI,
'%length' => drupal_strlen($menu_name),
)));
// Unlike most other modules, there is no confirmation message displayed.
$this->drupalGet('admin/structure/menu');
......
......@@ -94,41 +94,20 @@ function node_type_form($form, &$form_state, $type = NULL) {
'#description' => t('The human-readable name of this content type. This text will be displayed as part of the list on the <em>Add new content</em> page. It is recommended that this name begin with a capital letter and contain only letters, numbers, and spaces. This name must be unique.'),
'#required' => TRUE,
'#size' => 30,
'#field_suffix' => ' <small id="edit-name-suffix">' . ($type->locked ? t('Machine name: @name', array('@name' => $type->type)) : '&nbsp;') . '</small>',
);
if (!$type->locked) {
$js_settings = array(
'type' => 'setting',
'data' => array(
'machineReadableValue' => array(
'name' => array(
'text' => t('Machine name'),
'target' => 'type',
'searchPattern' => '[^a-z0-9]+',
'replaceToken' => '_',
),
),
),
);
$form['type'] = array(
'#title' => t('Machine name'),
'#type' => 'textfield',
'#default_value' => $type->type,
'#maxlength' => 32,
'#required' => TRUE,
'#description' => t('The machine-readable name of this content type. This text will be used for constructing the URL of the <em>add new content</em> page for this content type. This name must contain only lowercase letters, numbers, and underscores. Underscores will be converted into hyphens when constructing the URL of the <em>add new content</em> page. This name must be unique.'),
'#attached' => array(
'js' => array(drupal_get_path('module', 'system') . '/system.js', $js_settings),
),
);
}
else {
$form['type'] = array(
'#type' => 'value',
'#value' => $type->type,
);
}
$form['type'] = array(
'#type' => 'machine_name',
'#default_value' => $type->type,
'#maxlength' => 32,
'#disabled' => $type->locked,
'#machine_name' => array(
'exists' => 'node_type_load',
),
'#description' => t('A unique machine-readable name for this content type. It must only contain lowercase letters, numbers, and underscores. This name will be used for constructing the URL of the %node-add page, in which underscores will be converted into hyphens.', array(
'%node-add' => t('Add new content'),
)),
);
$form['description'] = array(
'#title' => t('Description'),
......@@ -276,12 +255,6 @@ function node_type_form_validate($form, &$form_state) {
$types = node_type_get_names();
if (!$form_state['values']['locked']) {
if (isset($types[$type->type]) && $type->type != $old_type) {
form_set_error('type', t('The machine-readable name %type is already taken.', array('%type' => $type->type)));
}
if (!preg_match('!^[a-z0-9_]+$!', $type->type)) {
form_set_error('type', t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
}
// 'theme' conflicts with theme_node_form().
// '0' is invalid, since elsewhere we check it using empty().
if (in_array($type->type, array('0', 'theme'))) {
......
......@@ -45,6 +45,16 @@ div.admin .expert-link {
margin: 0 0 0.5em 0;
}
/**
* Quick inline admin links.
*/
small .admin-link:before {
content: '[';
}
small .admin-link:after {
content: ']';
}
/**
* Modules page.
*/
......
......@@ -2068,28 +2068,12 @@ function system_add_date_format_type_form($form, &$form_state) {
'#title' => t('Date type'),
'#type' => 'textfield',
'#required' => TRUE,
'#field_suffix' => ' <small id="edit-date-type-suffix">&nbsp;</small>',
);
$js_settings = array(
'type' => 'setting',
'data' => array(
'machineReadableValue' => array(
'date-type' => array(
'text' => t('Machine name'),
'target' => 'machine-name',
'searchPattern' => '[^a-z0-9]+',
'replaceToken' => '_',
),
),
),
);
$form['machine_name'] = array(
'#title' => t('Machine readable name'),
'#description' => t('The unique machine readable name for this date type, can only contain lowercase letters, numbers and underscores.'),
'#type' => 'textfield',
'#required' => TRUE,
'#attached' => array(
'js' => array(drupal_get_path('module', 'system') . '/system.js', $js_settings),
'#type' => 'machine_name',
'#machine_name' => array(
'exists' => 'system_get_date_types',
'source' => array('date_type'),
),
);
......
......@@ -148,78 +148,4 @@ Drupal.behaviors.pageCache = {
}
};
/**
* Attach the auto machine readable name behavior.
*
* Settings are expected to be an object of elements to process, where the key
* defines the source element in the form and the value is an object defining
* the following properties:
* - text: The label to display before the auto-generated value.
* - target: The target form element name.
* - searchPattern: A regular expression (without modifiers) matching disallowed
* characters in the machine readable name, f.e. '[^a-z0-9]+'.
* - replaceToken: A replacement string to replace disallowed characters, f.e.
* '-' or '_'.
*
* @see menu_edit_menu()
*/
Drupal.behaviors.machineReadableValue = {
attach: function () {
var self = this;
for (var value in Drupal.settings.machineReadableValue) {
var settings = Drupal.settings.machineReadableValue[value];
// Build selector for the source name entered by a user.
var source = '#edit-' + value;
var suffix = source + '-suffix';
// Build selector for the machine readable name.
var target = '#edit-' + settings.target;
// Build selector for the wrapper element around the target field.
var wrapper = '.form-item-' + settings.target;
// Do not process the element if we got an error or the given name and the
// machine readable name are identical or the machine readable name is
// empty.
if (!$(target).hasClass('error') && ($(target).val() == '' || $(target).val() == self.transliterate($(source).val(), settings))) {
// Hide wrapper element.
$(wrapper).hide();
// Bind keyup event to source element.
$(source).keyup(function () {
var machine = self.transliterate($(this).val(), settings);
if (machine != '_' && machine != '') {
// Set machine readable name to the user entered value.
$(target).val(machine);
// Append the machine readable name and a link to edit it to the source field.
$(suffix).empty().append(' ' + settings.text + ': ' + machine + ' [').append($('<a href="#">' + Drupal.t('Edit') + '</a>').click(function () {
$(wrapper).show();
$(target).focus();
$(suffix).hide();
$(source).unbind('keyup');
return false;
})).append(']');
}
else {
$(target).val(machine);
$(suffix).text('');
}
});
// Call keyup event on source element.
$(source).keyup();
}
}
},
/**
* Transliterate a human-readable name to a machine name.
*
* The result should not contain any character matching settings.searchPattern,
* invalid characters are typically replaced with settings.replaceToken.
*/
transliterate: function (source, settings) {
var searchPattern = new RegExp(settings.searchPattern, 'g');
return source.toLowerCase().replace(searchPattern, settings.replaceToken);
}
};
})(jQuery);
......@@ -347,6 +347,18 @@ function system_element_info() {
'#theme' => 'textfield',
'#theme_wrappers' => array('form_element'),
);
$types['machine_name'] = array(
'#input' => TRUE,
'#default_value' => NULL,
'#required' => TRUE,
'#maxlength' => 64,
'#size' => 60,
'#autocomplete_path' => FALSE,
'#process' => array('form_process_machine_name', 'ajax_process_form'),
'#element_validate' => array('form_validate_machine_name'),
'#theme' => 'textfield',
'#theme_wrappers' => array('form_element'),
);
$types['password'] = array(
'#input' => TRUE,
'#size' => 60,
......
......@@ -948,7 +948,7 @@ class DateTimeFunctionalTest extends DrupalWebTestCase {
// Add custom date type.
$this->clickLink(t('Add date type'));
$date_type = $this->randomName(8);
$date_type = strtolower($this->randomName(8));
$machine_name = 'machine_' . $date_type;
$date_format = 'd.m.Y - H:i';
$edit = array(
......
......@@ -137,30 +137,13 @@ function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
'#default_value' => $vocabulary->name,
'#maxlength' => 255,
'#required' => TRUE,
'#field_suffix' => ' <small id="edit-name-suffix">&nbsp;</small>',
);
$js_settings = array(
'type' => 'setting',
'data' => array(
'machineReadableValue' => array(
'name' => array(
'text' => t('Machine name'),
'target' => 'machine-name',
'searchPattern' => '[^a-z0-9]+',
'replaceToken' => '_',
),
),
),
);
$form['machine_name'] = array(
'#type' => 'textfield',
'#title' => t('Machine-readable name'),
'#type' => 'machine_name',
'#default_value' => $vocabulary->machine_name,
'#maxlength' => 255,
'#description' => t('The unique machine-readable name for this vocabulary, used for theme templates. Can only contain lowercase letters, numbers, and underscores.'),
'#required' => TRUE,
'#attached' => array(
'js' => array(drupal_get_path('module', 'system') . '/system.js', $js_settings),
'#maxlength' => 21,
'#machine_name' => array(
'exists' => 'taxonomy_vocabulary_machine_name_load',
),
);
$form['old_machine_name'] = array(
......@@ -189,34 +172,6 @@ function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
return $form;
}
/**
* Validation handler for the vocabulary form.
*
* @see taxonomy_form_vocabulary()
*/
function taxonomy_form_vocabulary_validate($form, &$form_state) {
if ($form_state['clicked_button']['#value'] != t('Delete') && isset($form_state['values']['machine_name'])) {
// Restrict machine names to appropriate characters.
$machine_name = $form_state['values']['machine_name'];
if (!preg_match('!^[a-z0-9_]+$!', $form_state['values']['machine_name'])) {
form_set_error('machine_name', t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
}
// Restrict machine names to 21 characters to avoid exceeding the limit
// for field names.
if (drupal_strlen($machine_name) > 21) {
form_set_error('machine_name', t('The machine-readable name must not exceed 21 characters.'));
}
// Do not allow duplicate machine names.
$vocabularies = taxonomy_get_vocabularies();
foreach ($vocabularies as $vocabulary) {
if ($machine_name == $vocabulary->machine_name && (!isset($form_state['values']['vid']) || $vocabulary->vid != $form_state['values']['vid'])) {
form_set_error('machine_name', t('This machine-readable name is already in use by another vocabulary and must be unique.'));
}
}
}
}
/**
* Accept the form submission for a vocabulary and save the results.
*/
......
......@@ -92,7 +92,7 @@ class TaxonomyVocabularyFunctionalTest extends TaxonomyWebTestCase {
// Try to submit a vocabulary with a duplicate machine name.
$edit['machine_name'] = $machine_name;
$this->drupalPost('admin/structure/taxonomy/add', $edit, t('Save'));
$this->assertText(t('This machine-readable name is already in use by another vocabulary and must be unique.'), t('Duplicate machine name validation was successful'));
$this->assertText(t('The machine-readable name is already in use. It must be unique.'));
// Try to submit an invalid machine name.
$edit['machine_name'] = '!&^%';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment