1

I hope everyone is having a great day!

I am creating this question as I have been stuck on this issue all day and I need some friendly advice.

I have created a repeater custom metabox item for a custom post type called Proposal. The repeater works fine and is saving the content and displaying it how I would like. However, when I paste content into the textarea it throws a 403 error page when you update or publish the post and have MORE than 1 repeated option.

The weird part is when you just add text to the textarea and don't paste the content everything works fine. Obviously, this is an issue I need to fix before I release the plugin to the world. Here is the code I have. If you notice anything wrong, please let me know so I can improve it as I am fairly new to backend development.

<div id="wpp_meta_inner">
<?php
//get the saved meta as an array
$service = get_post_meta($post->ID,'service',false);
$service_title = get_post_meta($post->ID, 'wpp_service_title', true);
?>
    <div>

          <div class="wpp-input-container">
              <label><?php _e('Title') ?></label>
              <input type="text" name="wpp_service_title" id="wpp_service_title" value="<?php echo $service_title; ?>" />
              <p><?php _e('Will display default "Scope of Services" if empty.') ?></p>
          </div>

          <div class="wpp-input-container">
              <label class="wpp-label-repeater"><?php _e('Service Items') ?></label>
              <?php
              wp_nonce_field ( 'c_nonce_field', 'c_wpnonce');
                  $c = 0;
                  if ( count( $service ) > 0 ) {
                      if(!empty($service)) {
                          foreach( $service as $service_item_val ) {
                              foreach( $service_item_val as $service_item ) {
                                  if ( isset( $service_item['title'] ) || isset( $service_item['service_item'] ) ) {
                                      printf( '<div class="wpp-repeater-wrapper service">Title: <input class="wpp-repeater-input service" type="text" name="service[%1$s][title]" value="%2$s" />Description: <textarea class="wpp-repeater-input service" name="service[%1$s][service_item]" data-gramm_editor="false" value="">%3$s</textarea><span class="wpp-item-remove service">%4$s</span></div>', $c, $service_item['title'], $service_item['service_item'], __( 'Remove' ) );
                                      $c = $c +1;
                                  }
                              }
                          }
                      }
                  }
              ?>
              <span id="services_here"></span>
              <div class="wpp-item-add services" style="visibility: hidden; margin-bottom: -20px;"><?php _e('Add Item'); ?></div>
              <div class="wpp-item-add services add-button"><?php _e('Add Service'); ?></div>
              </div>

    </div>

    <script>
    var $ =jQuery.noConflict();
    $(document).ready(function() {
        var count = <?php echo $c; ?>;
        $(".wpp-item-add.services").click(function() {
            count = count + 1;
            $('#services_here').append('<div class="wpp-repeater-wrapper service">Title: <input class="wpp-repeater-input service" type="text" name="service['+count+'][title]" value="" placeholder="" />Description: <textarea class="wpp-repeater-input service" name="service['+count+'][service_item]" data-gramm_editor="false" value="" placeholder=""></textarea><span class="wpp-item-remove service">Remove</span></div>' );
            return false;
        });
        $(".wpp-item-remove.service").live('click', function() {
            $(this).parent().remove();
        });
    });
    </script>

</div>

<?php }
/* When the post is saved, saves our data */
function wpp_save_services_data( $post_id ) {
if(defined("DOING_AJAX") AND DOING_AJAX)
  return;
  if(!current_user_can('edit_post', $post_id ))
  return;
  if(!isset($_POST['c_wpnonce']) || !wp_verify_nonce( $_POST['c_wpnonce'], 'c_nonce_field'))
  return;
  $allowed_html = array(
          'a' => array(
              'href' => array()
          ));
  $service = $_POST['service'];
  update_post_meta ($post_id, 'wpp_service_title', wp_kses( $_POST['wpp_service_title'], $allowed_html));
  update_post_meta ($post_id,'service',$service);
 }
  • I tested your code and could not duplicate the issue. Maybe there is some other code somewhere causing the issue. – Dave Romsey Dec 13 '18 at 6:10
  • Hey Dave, thanks for commenting back. I have finally debugged the issue and determined it was a mod_security rule that was getting triggered from the hosting company. The reason why it would trigger only when pasting content is because the content I was pasting containged the text (CMS). For some reason anytime the textarea contained () it would trigger mod_security. Do you happen to have a solution that would trip those values from the textarea before it gets saved or on blur? – Kevin W. Dec 13 '18 at 15:07
  • You got it! I posted an answer with some updated code. Not 100% sure it will fix the issue (let me know), but I was easily able to break the input fields with certain data before, and with the updated code, things are working as expected. – Dave Romsey Dec 13 '18 at 19:24
1

I gave your code a look over, and updated it to include proper escaping and sanitation. I suspect that the problem could have stemmed from the lack of escaping. I also applied some formatting and WP best practices, but otherwise the code is the same.

/**
 * Meta box display callback.
 *
 * @param WP_Post $post Current post object.
 */
function wpse_metabox_callback( $post ) {
    $service_title = get_post_meta( $post->ID, 'wpp_service_title', true );
    $service       = get_post_meta( $post->ID, 'service', false ); // get as an array ?>

    <div id="wpp_meta_inner">
    <div>
            <div class="wpp-input-container">
                <label><?php esc_html_e( 'Title', 'textdomain' ); ?></label>
                <input type="text" name="wpp_service_title" id="wpp_service_title" value="<?php echo esc_attr( $service_title ); ?>" />
                <p><?php esc_html_e( 'Will display default "Scope of Services" if empty.', 'textdomain' ); ?></p>
            </div>

            <div class="wpp-input-container">
                <label class="wpp-label-repeater"><?php _e( 'Service Items', 'textdomain' ); ?></label>
                <?php
                    wp_nonce_field( 'c_nonce_field', 'c_wpnonce' );
                    $c = 0;
                    if ( count( $service ) > 0 ) {
                        if ( ! empty( $service ) ) {
                            foreach( $service as $service_item_val ) {
                                foreach( $service_item_val as $service_item ) {
                                    if ( isset( $service_item['title'] ) || isset( $service_item['service_item'] ) ) {
                                        printf(
                                            '<div class="wpp-repeater-wrapper service">' .
                                                'Title: <input class="wpp-repeater-input service" type="text" name="service[%1$s][title]" value="%2$s" />' .
                                                'Description: <textarea class="wpp-repeater-input service" name="service[%1$s][service_item]" data-gramm_editor="false" value="">%3$s</textarea><span class="wpp-item-remove service">%4$s</span>' . 
                                            '</div>',
                                            esc_attr( $c ),
                                            esc_html( $service_item['title'] ),
                                            esc_textarea( $service_item['service_item'] ),
                                            esc_html( __( 'Remove', 'textdomain' ) )
                                        );
                                        $c += 1;
                                    }
                                }
                            }
                        }
                    }
                ?>
                <span id="services_here"></span>
                <div class="wpp-item-add services" style="visibility: hidden; margin-bottom: -20px;"><?php esc_html_e( 'Add Item', 'textdomain' ); ?></div>
                <div class="wpp-item-add services add-button"><?php esc_html_e( 'Add Service', 'textdomain' ); ?></div>
            </div>
        </div>

        <script>
            var $ =jQuery.noConflict();
            $(document).ready(function() {
                    var count = <?php echo esc_js( $c ); ?>;
                    $(".wpp-item-add.services").click(function() {
                            count = count + 1;
                            $('#services_here').append('<div class="wpp-repeater-wrapper service">Title: <input class="wpp-repeater-input service" type="text" name="service['+count+'][title]" value="" placeholder="" />Description: <textarea class="wpp-repeater-input service" name="service['+count+'][service_item]" data-gramm_editor="false" value="" placeholder=""></textarea><span class="wpp-item-remove service">Remove</span></div>' );
                            return false;
                    });
                    $(".wpp-item-remove.service").live('click', function() {
                            $(this).parent().remove();
                    });
            });
        </script>
    </div><?php
}

When saving the metabox data, wp_kses_post() will be applied to each element of the $services array:

/**
 * Sanitize and save metabox data.
 */
add_action( 'save_post', 'wpse_save_meta_box' );
function wpse_save_meta_box( $post_id ) {
    if ( defined( 'DOING_AJAX' ) AND DOING_AJAX ) {
        return;
    }

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    if ( ! isset( $_POST['c_wpnonce'] ) || ! wp_verify_nonce( $_POST['c_wpnonce'], 'c_nonce_field' ) ) {
        return;
    }

    $allowed_html = array (
        'a' => array(
            'href' => array()
    ) );

    $service = isset( $_POST['service'] ) && ! empty( $_POST['service'] ) ? array_map( 'wp_kses_post', $_POST['service'] ) : array();

    update_post_meta( $post_id, 'wpp_service_title', wp_kses( $_POST['wpp_service_title'], $allowed_html ) );
    update_post_meta( $post_id, 'service', $service );
}

For the sake of completeness, here is the code that registers the proposal post type:

/**
 * Register proposal post type.
 */
add_action( 'init', 'wpse_register_post_type_proposal' );
function wpse_register_post_type_proposal() {
    $args = [
            'label'             => __( 'Proposals', 'textdomain' ),
            'public'             => true,
            'publicly_queryable' => true,
            'show_ui'            => true,
            'show_in_menu'       => true,
            'query_var'          => true,
            'rewrite'            => [ 'slug' => 'proposal' ],
            'capability_type'    => 'post',
            'has_archive'        => true,
            'hierarchical'       => false,
            'menu_position'      => null,
            'supports'           => [ 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' ],
    ];
    register_post_type( 'proposal', $args );
}

and the meta box:

/**
 * Register meta box
 */
function wpse_register_meta_boxes() {
    add_meta_box( 'meta-box-id', __( 'My Meta Box', 'textdomain' ), 'wpse_metabox_callback', 'proposal' );
}
add_action( 'add_meta_boxes', 'wpse_register_meta_boxes' );
  • 1
    Holy crap Dave this is awesome! Also, thank you for including the WordPress best practices! I started as a Frontend developer but am trying to learn more backend development, hence why I am creating some free much-needed plugins in my opinion. I am not able to test the code currently as my hosting company white labeled the mod_security rule that was causing the issue, but I am going to reach out to them and see if they can reactivate it so I can test it out. Thanks for all your help and knowledge. Much appreciated, Kevin W. – Kevin W. Dec 13 '18 at 21:49
  • Happy to help, Kevin! A rule of thumb is to never trust user input. Sanitize all incoming data and escape all output using the context appropriate technique (different approach for HTML, HTML attributes, and JS for example, which were all used in this code). The Codex entry on Data Validation has a good rundown of WP core functions to help with this. – Dave Romsey Dec 13 '18 at 22:53
  • Hey Dave - I have a question about the code your provided which I have created an answer below since it was too long. Thanks in advance man! – Kevin W. Mar 10 at 13:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.