diff --git a/core/includes/common.inc b/core/includes/common.inc index ef0778a..c096435 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -6927,6 +6927,9 @@ function drupal_common_theme() { 'tel' => array( 'render element' => 'element', ), + 'url' => array( + 'render element' => 'element', + ), 'form' => array( 'render element' => 'element', ), diff --git a/core/includes/form.inc b/core/includes/form.inc index 853990b..3b63553 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3786,6 +3786,56 @@ function theme_tel($variables) { } /** + * Returns HTML for a url form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #placeholder, #required, #attributes, #autocomplete_path. + * + * @ingroup themeable + */ +function theme_url($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'url'; + element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); + _form_set_class($element, array('form-url')); + + $extra = ''; + if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) { + drupal_add_library('system', 'drupal.autocomplete'); + $element['#attributes']['class'][] = 'form-autocomplete'; + + $attributes = array(); + $attributes['type'] = 'hidden'; + $attributes['id'] = $element['#attributes']['id'] . '-autocomplete'; + $attributes['value'] = url($element['#autocomplete_path'], array('absolute' => TRUE)); + $attributes['disabled'] = 'disabled'; + $attributes['class'][] = 'autocomplete'; + $extra = ''; + } + + $output = ''; + + return $output . $extra; +} + +/** + * Form element validation handler for #type 'url'. + * + * Note that #maxlength and #required is validated by _form_validate() already. + */ +function form_validate_url(&$element, &$form_state) { + $value = trim($element['#value']); + form_set_value($element, $value, $form_state); + + if ($value !== '' && !valid_url($value, TRUE)) { + form_error($element, t('The URL %url is not valid.', array('%url' => $value))); + } +} + +/** * Returns HTML for a form. * * @param $variables diff --git a/core/modules/aggregator/aggregator.admin.inc b/core/modules/aggregator/aggregator.admin.inc index 93319bf..f5cf8e5 100644 --- a/core/modules/aggregator/aggregator.admin.inc +++ b/core/modules/aggregator/aggregator.admin.inc @@ -81,7 +81,8 @@ function aggregator_form_feed($form, &$form_state, stdClass $feed = NULL) { '#description' => t('The name of the feed (or the name of the website providing the feed).'), '#required' => TRUE, ); - $form['url'] = array('#type' => 'textfield', + $form['url'] = array( + '#type' => 'url', '#title' => t('URL'), '#default_value' => isset($feed->url) ? $feed->url : '', '#maxlength' => 255, @@ -146,10 +147,6 @@ function aggregator_form_feed($form, &$form_state, stdClass $feed = NULL) { */ function aggregator_form_feed_validate($form, &$form_state) { if ($form_state['values']['op'] == t('Save')) { - // Ensure URL is valid. - if (!valid_url($form_state['values']['url'], TRUE)) { - form_set_error('url', t('The URL %url is invalid. Enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $form_state['values']['url']))); - } // Check for duplicate titles. if (isset($form_state['values']['fid'])) { $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE (title = :title OR url = :url) AND fid <> :fid", array(':title' => $form_state['values']['title'], ':url' => $form_state['values']['url'], ':fid' => $form_state['values']['fid'])); @@ -268,7 +265,7 @@ function aggregator_form_opml($form, &$form_state) { '#description' => t('Upload an OPML file containing a list of feeds to be imported.'), ); $form['remote'] = array( - '#type' => 'textfield', + '#type' => 'url', '#title' => t('OPML Remote URL'), '#maxlength' => 1024, '#description' => t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'), @@ -316,11 +313,6 @@ function aggregator_form_opml_validate($form, &$form_state) { if (empty($form_state['values']['remote']) == empty($_FILES['files']['name']['upload'])) { form_set_error('remote', t('You must either upload a file or enter a URL.')); } - - // Validate the URL, if one was entered. - if (!empty($form_state['values']['remote']) && !valid_url($form_state['values']['remote'], TRUE)) { - form_set_error('remote', t('This URL is not valid.')); - } } /** diff --git a/core/modules/aggregator/aggregator.test b/core/modules/aggregator/aggregator.test index 27c4673..e2dc4af 100644 --- a/core/modules/aggregator/aggregator.test +++ b/core/modules/aggregator/aggregator.test @@ -671,7 +671,7 @@ class ImportOPMLTestCase extends AggregatorTestCase { $edit = array('remote' => 'invalidUrl://empty'); $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import')); - $this->assertText(t('This URL is not valid.'), t('Error if the URL is invalid.')); + $this->assertText(t('The URL invalidUrl://empty is not valid.'), t('Error if the URL is invalid.')); $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField(); $this->assertEqual($before, $after, t('No feeds were added during the three last form submissions.')); diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 9ad7cfc..3e8549a 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1793,7 +1793,7 @@ function comment_form($form, &$form_state, $comment) { '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), ); $form['author']['homepage'] = array( - '#type' => 'textfield', + '#type' => 'url', '#title' => t('Homepage'), '#default_value' => $comment->homepage, '#maxlength' => 255, @@ -1986,9 +1986,6 @@ function comment_form_validate($form, &$form_state) { if ($form_state['values']['mail'] && !valid_email_address($form_state['values']['mail'])) { form_set_error('mail', t('The e-mail address you specified is not valid.')); } - if ($form_state['values']['homepage'] && !valid_url($form_state['values']['homepage'], TRUE)) { - form_set_error('homepage', t('The URL of your homepage is not valid. Remember that it must be fully qualified, i.e. of the form http://example.com/directory.')); - } } /** diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index c9fc562..5b23270 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -2243,6 +2243,7 @@ class DrupalWebTestCase extends DrupalTestCase { case 'text': case 'tel': case 'textarea': + case 'url': case 'hidden': case 'password': $post[$name] = $edit[$name]; diff --git a/core/modules/simpletest/tests/form.test b/core/modules/simpletest/tests/form.test index 784da88..eb77d6b 100644 --- a/core/modules/simpletest/tests/form.test +++ b/core/modules/simpletest/tests/form.test @@ -40,6 +40,9 @@ class FormsTestCase extends DrupalWebTestCase { $elements['telephone']['element'] = array('#title' => $this->randomName(), '#type' => 'tel'); $elements['telephone']['empty_values'] = $empty_strings; + $elements['url']['element'] = array('#title' => $this->randomName(), '#type' => 'url'); + $elements['url']['empty_values'] = $empty_strings; + $elements['password']['element'] = array('#title' => $this->randomName(), '#type' => 'password'); $elements['password']['empty_values'] = $empty_strings; @@ -261,7 +264,7 @@ class FormsTestCase extends DrupalWebTestCase { // All the elements should be marked as disabled, including the ones below // the disabled container. - $this->assertEqual(count($disabled_elements), 33, t('The correct elements have the disabled property in the HTML code.')); + $this->assertEqual(count($disabled_elements), 34, 'The correct elements have the disabled property in the HTML code.'); $this->drupalPost(NULL, $edit, t('Submit')); $returned_values['hijacked'] = drupal_json_decode($this->content); @@ -397,7 +400,7 @@ class FormElementTestCase extends DrupalWebTestCase { $expected = 'placeholder-text'; // Test to make sure textfields and passwords have the proper placeholder // text. - foreach (array('textfield', 'password') as $type) { + foreach (array('textfield', 'url', 'password') as $type) { $element = $this->xpath('//input[@id=:id and @placeholder=:expected]', array( ':id' => 'edit-' . $type, ':expected' => $expected, @@ -1628,3 +1631,41 @@ class FormCheckboxTestCase extends DrupalWebTestCase { } } } + +/** + * Tests url element. + */ +class FormUrlTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Form API url', + 'description' => 'Tests the form API url element.', + 'group' => 'Form API', + ); + } + + public function setUp() { + parent::setUp('form_test'); + } + + /** + * Tests that #type 'url' fields are properly validated and trimmed. + */ + function testFormUrl() { + $edit = array(); + $edit['url'] = 'http://'; + $edit['url_required'] = ' '; + $this->drupalPost('form-test/url', $edit, 'Submit'); + $this->assertRaw(t('The URL %url is not valid.', array('%url' => 'http://'))); + $this->assertRaw(t('!name field is required.', array('!name' => 'Required URL'))); + + $edit = array(); + $edit['url'] = "\n"; + $edit['url_required'] = 'http://example.com/ '; + $values = drupal_json_decode($this->drupalPost('form-test/url', $edit, 'Submit')); + $this->assertIdentical($values['url'], ''); + $this->assertEqual($values['url_required'], 'http://example.com/'); + } +} diff --git a/core/modules/simpletest/tests/form_test.module b/core/modules/simpletest/tests/form_test.module index e1e2435..1d89676 100644 --- a/core/modules/simpletest/tests/form_test.module +++ b/core/modules/simpletest/tests/form_test.module @@ -125,6 +125,12 @@ function form_test_menu() { 'page arguments' => array('form_test_checkboxes_radios'), 'access callback' => TRUE, ); + $items['form-test/url'] = array( + 'title' => t('URL'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_url'), + 'access callback' => TRUE, + ); $items['form-test/disabled-elements'] = array( 'title' => t('Form test'), @@ -1027,7 +1033,7 @@ function form_test_select_submit($form, &$form_state) { * Builds a form to test the placeholder attribute. */ function form_test_placeholder_test($form, &$form_state) { - foreach (array('textfield', 'textarea', 'password', 'tel') as $type) { + foreach (array('textfield', 'textarea', 'url', 'password', 'tel') as $type) { $form[$type] = array( '#type' => $type, '#title' => $type, @@ -1098,6 +1104,39 @@ function form_test_checkboxes_radios($form, &$form_state, $customize = FALSE) { } /** + * Form consructor for testing #type 'url' elements. + * + * @see form_test_url_submit() + * @ingroup forms + */ +function form_test_url($form, &$form_state) { + $form['url'] = array( + '#type' => 'url', + '#title' => 'Optional URL', + '#description' => 'An optional URL field.', + ); + $form['url_required'] = array( + '#type' => 'url', + '#title' => 'Required URL', + '#description' => 'A required URL field.', + '#required' => TRUE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Submit', + ); + return $form; +} + +/** + * Form submission handler for form_test_url(). + */ +function form_test_url_submit($form, &$form_state) { + drupal_json_output($form_state['values']); + exit(); +} + +/** * Build a form to test disabled elements. */ function _form_test_disabled_elements($form, &$form_state) { @@ -1205,6 +1244,14 @@ function _form_test_disabled_elements($form, &$form_state) { ); } + // URL. + $form['disabled_container']['disabled_container_url']= array( + '#type' => 'url', + '#title' => 'url', + '#default_value' => 'http://example.com', + '#test_hijack_value' => 'http://example.com/foo', + ); + // Text format. $form['text_format'] = array( '#type' => 'text_format', diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 38c3f55..d7ceb74 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -374,6 +374,16 @@ function system_element_info() { '#theme' => 'tel', '#theme_wrappers' => array('form_element'), ); + $types['url'] = array( + '#input' => TRUE, + '#size' => 60, + '#maxlength' => 255, + '#autocomplete_path' => FALSE, + '#process' => array('ajax_process_form'), + '#element_validate' => array('form_validate_url'), + '#theme' => 'url', + '#theme_wrappers' => array('form_element'), + ); $types['machine_name'] = array( '#input' => TRUE, '#default_value' => NULL, diff --git a/core/modules/update/update.manager.inc b/core/modules/update/update.manager.inc index d9fd86f..f56dd0b 100644 --- a/core/modules/update/update.manager.inc +++ b/core/modules/update/update.manager.inc @@ -492,7 +492,7 @@ function update_manager_install_form($form, &$form_state, $context) { ); $form['project_url'] = array( - '#type' => 'textfield', + '#type' => 'url', '#title' => t('Install from a URL'), '#description' => t('For example: %url', array('%url' => 'http://ftp.drupal.org/files/projects/name.tar.gz')), ); @@ -592,12 +592,6 @@ function update_manager_install_form_validate($form, &$form_state) { if (!($form_state['values']['project_url'] XOR !empty($_FILES['files']['name']['project_upload']))) { form_set_error('project_url', t('You must either provide a URL or upload an archive file to install.')); } - - if ($form_state['values']['project_url']) { - if (!valid_url($form_state['values']['project_url'], TRUE)) { - form_set_error('project_url', t('The provided URL is invalid.')); - } - } } /** diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css index 850a69c..f768597 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -1200,6 +1200,7 @@ select.form-select { } input.form-text, input.form-tel, +input.form-url, textarea.form-textarea, select.form-select { border: 1px solid #ccc; diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css index 301659c..5a1c89e 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -602,6 +602,7 @@ div.teaser-checkbox .form-item, .form-disabled input.form-autocomplete, .form-disabled input.form-text, .form-disabled input.form-tel, +.form-disabled input.form-url, .form-disabled input.form-file, .form-disabled textarea.form-textarea, .form-disabled select.form-select { @@ -689,6 +690,7 @@ input.form-button-disabled:active { input.form-autocomplete, input.form-text, input.form-tel, +input.form-url, input.form-file, textarea.form-textarea, select.form-select { @@ -703,6 +705,7 @@ select.form-select { } input.form-text:focus, input.form-tel:focus, +input.form-url:focus, input.form-file:focus, textarea.form-textarea:focus, select.form-select:focus {