Skip to content

Commit

Permalink
Contact Form: Allow for commas and such in field labels, options, and…
Browse files Browse the repository at this point in the history
… values (#17335)
  • Loading branch information
anomiex authored Oct 15, 2020
1 parent 826d47d commit c663b90
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 40 deletions.
38 changes: 33 additions & 5 deletions modules/contact-form/grunion-contact-form.php
Original file line number Diff line number Diff line change
Expand Up @@ -2366,6 +2366,34 @@ static function remove_empty( $single_value ) {
return ( $single_value !== '' );
}

/**
* Escape a shortcode value.
*
* Shortcode attribute values have a number of unfortunate restrictions, which fortunately we
* can get around by adding some extra HTML encoding.
*
* The output HTML will have a few extra escapes, but that makes no functional difference.
*
* @since 9.1.0
* @param string $val Value to escape.
* @return string
*/
private static function esc_shortcode_val( $val ) {
return strtr(
esc_html( $val ),
array(
// Brackets in attribute values break the shortcode parser.
'[' => '[',
']' => ']',
// Shortcode parser screws up backslashes too, thanks to calls to `stripcslashes`.
'\\' => '\',
// The existing code here represents arrays as comma-separated strings.
// Rather than trying to change representations now, just escape the commas in values.
',' => ',',
)
);
}

/**
* The contact-field shortcode processor
* We use an object method here instead of a static Grunion_Contact_Form_Field class method to parse contact-field shortcodes so that we can tie them to the contact-form object.
Expand All @@ -2384,18 +2412,18 @@ static function parse_contact_field( $attributes, $content ) {
}
foreach ( $attributes as $att => $val ) {
if ( is_numeric( $att ) ) { // Is a valueless attribute
$att_strs[] = esc_html( $val );
$att_strs[] = self::esc_shortcode_val( $val );
} elseif ( isset( $val ) ) { // A regular attr - value pair
if ( ( $att === 'options' || $att === 'values' ) && is_string( $val ) ) { // remove any empty strings
$val = explode( ',', $val );
}
if ( is_array( $val ) ) {
if ( is_array( $val ) ) {
$val = array_filter( $val, array( __CLASS__, 'remove_empty' ) ); // removes any empty strings
$att_strs[] = esc_html( $att ) . '="' . implode( ',', array_map( 'esc_html', $val ) ) . '"';
$att_strs[] = esc_html( $att ) . '="' . implode( ',', array_map( array( __CLASS__, 'esc_shortcode_val' ), $val ) ) . '"';
} elseif ( is_bool( $val ) ) {
$att_strs[] = esc_html( $att ) . '="' . esc_html( $val ? '1' : '' ) . '"';
$att_strs[] = esc_html( $att ) . '="' . ( $val ? '1' : '' ) . '"';
} else {
$att_strs[] = esc_html( $att ) . '="' . esc_html( $val ) . '"';
$att_strs[] = esc_html( $att ) . '="' . self::esc_shortcode_val( $val ) . '"';
}
}
}
Expand Down
118 changes: 83 additions & 35 deletions tests/php/modules/contact-form/test-class.grunion-contact-form.php
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,33 @@ public function test_make_sure_that_we_remove_empty_options_from_form_field() {
$this->assertEquals( "[contact-field type=\"select\" required=\"1\" options=\"fun,run\" label=\"fun times\" values=\"go,have some fun\"/]", $html );
}

/**
* Tests shortcode with commas and brackets.
*
* @covers Grunion_Contact_Form_Field
*/
public function test_array_values_with_commas_and_brackets() {
add_shortcode( 'contact-field', array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
$shortcode = "[contact-field type='radio' options='\"foo\",bar, baz,[b\rackets]' label='fun ][ times'/]";
$html = do_shortcode( $shortcode );
$this->assertEquals( '[contact-field type="radio" options=""foo",bar, baz,[b\rackets]" label="fun ][ times"/]', $html );
}

/**
* Tests Gutenblock input with commas and brackets.
*
* @covers Grunion_Contact_Form_Field
*/
public function test_array_values_with_commas_and_brackets_from_gutenblock() {
$attr = array(
'type' => 'radio',
'options' => array( '"foo"', 'bar, baz', '[b\\rackets]' ),
'label' => 'fun ][ times',
);
$html = Grunion_Contact_Form_Plugin::gutenblock_render_field_radio( $attr, '' );
$this->assertEquals( '[contact-field type="radio" options=""foo",bar, baz,[b\rackets]" label="fun ][ times"/]', $html );
}

/**
* Test for text field_renders
*
Expand Down Expand Up @@ -690,13 +717,13 @@ public function test_make_sure_checkbox_multiple_field_renders_as_expected() {
*/
public function test_make_sure_radio_field_renders_as_expected() {
$attributes = array(
'label' => 'fun',
'type' => 'radio',
'class' => 'lalala',
'label' => 'fun',
'type' => 'radio',
'class' => 'lalala',
'default' => 'option 1',
'id' => 'funID',
'options' => array( 'option 1', 'option 2' ),
'values' => array( 'option 1', 'option 2' ),
'id' => 'funID',
'options' => array( 'option 1', 'option 2', 'option 3, or 4', 'back\\slash' ),
'values' => array( 'option 1', 'option 2', 'option [34]', '\\' ),
);

$expected_attributes = array_merge( $attributes, array( 'input_type' => 'radio' ) );
Expand All @@ -710,13 +737,13 @@ public function test_make_sure_radio_field_renders_as_expected() {
*/
public function test_make_sure_select_field_renders_as_expected() {
$attributes = array(
'label' => 'fun',
'type' => 'select',
'class' => 'lalala',
'label' => 'fun',
'type' => 'select',
'class' => 'lalala',
'default' => 'option 1',
'id' => 'funID',
'options' => array( 'option 1', 'option 2' ),
'values' => array( 'o1', 'o2' ),
'id' => 'funID',
'options' => array( 'option 1', 'option 2', 'option 3, or 4', 'back\\slash' ),
'values' => array( 'option 1', 'option 2', 'option [34]', '\\' ),
);

$expected_attributes = array_merge( $attributes, array( 'input_type' => 'select' ) );
Expand Down Expand Up @@ -842,17 +869,17 @@ public function assertValidCheckboxField( $html, $attributes ) {

public function assertValidFieldMultiField( $html, $attributes ) {

$wrapperDiv = $this->getCommonDiv( $html );
$this->assertCommonValidHtml( $wrapperDiv, $attributes );
$wrapper_div = $this->getCommonDiv( $html );
$this->assertCommonValidHtml( $wrapper_div, $attributes );

// Get label
$label = $this->getFirstElement( $wrapperDiv, 'label' );
$label = $this->getFirstElement( $wrapper_div, 'label' );

//Inputs
if ( $attributes['type'] === 'select' ) {
$this->assertEquals( $label->getAttribute( 'class' ), 'grunion-field-label select', 'label class doesn\'t match' );

$select = $this->getFirstElement( $wrapperDiv, 'select' );
$select = $this->getFirstElement( $wrapper_div, 'select' );
$this->assertEquals(
$label->getAttribute( 'for' ),
$select->getAttribute( 'id' ),
Expand All @@ -867,28 +894,49 @@ public function assertValidFieldMultiField( $html, $attributes ) {

$this->assertEquals( $select->getAttribute( 'class' ), 'select '. $attributes['class'], ' select class does not match expected' );

// First Option
$option = $this->getFirstElement( $select, 'option' );
$this->assertEquals( $option->getAttribute( 'value' ), $attributes['values'][0], 'Input value doesn\'t match' );
$this->assertEquals( $option->getAttribute( 'selected' ), 'selected', 'Input is not selected' );
$this->assertEquals( $option->nodeValue, $attributes['options'][0], 'Input does not match the option' );

// Options.
$options = $select->getElementsByTagName( 'option' );
$n = is_array( $options ) ? count( $options ) : $options->length;
$this->assertEquals( $n, count( $attributes['options'] ), 'Number of inputs doesn\'t match number of options' );
$this->assertEquals( $n, count( $attributes['values'] ), 'Number of inputs doesn\'t match number of values' );
for ( $i = 0; $i < $n; $i++ ) {
$option = is_array( $options ) ? $options[ $i ] : $options->item( $i );
$this->assertEquals( $option->getAttribute( 'value' ), $attributes['values'][ $i ], 'Input value doesn\'t match' );
if ( 0 === $i ) {
$this->assertEquals( $option->getAttribute( 'selected' ), 'selected', 'Input is not selected' );
} else {
$this->assertNotEquals( $option->getAttribute( 'selected' ), 'selected', 'Input is selected' );
}
//phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$this->assertEquals( $option->nodeValue, $attributes['options'][ $i ], 'Input does not match the option' );
}
} else {
$this->assertEquals( $label->getAttribute( 'class' ), 'grunion-field-label', 'label class doesn\'t match' );
// Radio and Checkboxes
$second_label = $this->getFirstElement( $wrapperDiv, 'label', 1 );
$this->assertEquals( $second_label->nodeValue, ' ' . $attributes['options'][0] ); // extra space added for a padding

$input = $this->getFirstElement( $second_label, 'input' );
$this->assertEquals( $input->getAttribute( 'type' ), $attributes['input_type'], 'Type doesn\'t match' );
if ( $attributes['input_type'] === 'radio' ) {
$this->assertEquals( $input->getAttribute( 'name' ), $attributes['id'], 'Input name doesn\'t match' );
} else {
$this->assertEquals( $input->getAttribute( 'name' ), $attributes['id'] . '[]', 'Input name doesn\'t match' );
// Radio and Checkboxes.
$labels = $wrapper_div->getElementsByTagName( 'label' );
$n = is_array( $labels ) ? count( $labels ) - 1 : $labels->length - 1;
$this->assertEquals( $n, count( $attributes['options'] ), 'Number of inputs doesn\'t match number of options' );
$this->assertEquals( $n, count( $attributes['values'] ), 'Number of inputs doesn\'t match number of values' );
for ( $i = 0; $i < $n; $i++ ) {
$item_label = is_array( $labels ) ? $labels[ $i + 1 ] : $labels->item( $i + 1 );
//phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$this->assertEquals( $item_label->nodeValue, ' ' . $attributes['options'][ $i ] ); // extra space added for a padding.

$input = $this->getFirstElement( $item_label, 'input' );
$this->assertEquals( $input->getAttribute( 'type' ), $attributes['input_type'], 'Type doesn\'t match' );
if ( 'radio' === $attributes['input_type'] ) {
$this->assertEquals( $input->getAttribute( 'name' ), $attributes['id'], 'Input name doesn\'t match' );
} else {
$this->assertEquals( $input->getAttribute( 'name' ), $attributes['id'] . '[]', 'Input name doesn\'t match' );
}
$this->assertEquals( $input->getAttribute( 'value' ), $attributes['values'][ $i ], 'Input value doesn\'t match' );
$this->assertEquals( $input->getAttribute( 'class' ), $attributes['type'] . ' ' . $attributes['class'], 'Input class doesn\'t match' );
if ( 0 === $i ) {
$this->assertEquals( $input->getAttribute( 'checked' ), 'checked', 'Input checked doesn\'t match' );
} else {
$this->assertNotEquals( $input->getAttribute( 'checked' ), 'checked', 'Input checked doesn\'t match' );
}
}
$this->assertEquals( $input->getAttribute( 'value' ), $attributes['values'][0], 'Input value doesn\'t match' );
$this->assertEquals( $input->getAttribute( 'class' ), $attributes['type'] . ' '. $attributes['class'], 'Input class doesn\'t match' );
$this->assertEquals( $input->getAttribute( 'checked' ), 'checked', 'Input checked doesn\'t match' );
}
}

Expand Down

0 comments on commit c663b90

Please sign in to comment.