1

I want to add a front end post editing functionality to one of my Wordpress sites. I found a few plugins that do this, but they don't fits all my needs, so I decided to adapt an existing solution to develop a my own plugin that will return with a shortcode a front end editing form. My plugin works, I can edit and save posts, but I can't solve a warning and I don't have (yet) sufficient knowledge to make this plugin more secure. Any suggestions?

The warning:

Warning: Cannot modify header information - headers already sent by (output started at .../wp-content/plugins/front-post-edit.php:139) in .../wp-includes/pluggable.php on line 1228

P.S. The 139 line is the last line in my plugin (a line without code).

My plugin code:

<?php
/*
 * Plugin Name: Front Post Editor
 * 
 */

add_shortcode( 'front_post_edit', 'post_shortcode' );

function post_shortcode() {
    return getForm();
}

function getForm() {

    if ( !is_user_logged_in()) {
        echo '<p class="alert-box notice">You must be <a href="' . esc_url( wp_login_url( get_permalink() ) ) . '" title="Login">logged in</a>!';
    } else {

        if( 'POST' == $_SERVER['REQUEST_METHOD'] && !empty( $_POST['action'] ) &&  $_POST['action'] == "edit_post" && isset($_POST['postid'])) {
            $post_to_edit = array();
            $post_to_edit = get_post($_POST['postid']);

            /* these are the fields that we are editing in the form below */
            $title = $_POST['item_title'];
            $description = $_POST['item_description'];
            $category = $_POST['item_category'];
            $location = $_POST['item_location'];
            $location2 = $_POST['item_location2'];

            /* this code will save the title and description into the post_to_edit array */
            $post_to_edit->post_title = $title;
            $post_to_edit->post_content = $description;

            /* this code is a must */
            $pid = wp_update_post($post_to_edit);

            /* save taxonomies: post ID, form field name, taxonomy name, if it appends(true) or rewrite(false) */
            wp_set_post_terms($pid, array($_POST['item_category']),'category',false);
            wp_set_post_terms($pid, array($_POST['item_location']),'location',false);

            /* update custom fields with the new info */
            update_post_meta($pid, 'item_location2', $location2);

            /* redirect user after done editing */
            wp_redirect( home_url( '/myposts' ) );

        }

        /* get post to edit */
        $post_to_edit = get_post($_POST['postid']);

        /* get this post's category taxonomy term id */
        $term_name = strip_tags( get_the_term_list( $post_to_edit->ID, 'category', '', ', ', '' ) );
        $term_obj = get_term_by('name', $term_name, 'category');
        $term_id = $term_obj->term_id;

        /* array for wp_dropdown_category to display with the current post category selected by default */
        $args_cat = array(
            'selected'           => $term_id,
            'name'               => 'item_category',
            'class'              => 'postform',
            'tab_index'          => 10,
            'depth'              => 2,
            'hierarchical'       => 1,
            'taxonomy'           => 'category',
            'hide_empty'      => false );

        /* get this post's location taxonomy term id */
        $term_name2 = strip_tags( get_the_term_list( $post_to_edit->ID, 'location', '', ', ', '' ) );
        $term_obj2 = get_term_by('name', $term_name2, 'location');
        $term_id2 = $term_obj2->term_id;

        $args_loc = array(
            'selected'           => $term_id2,
            'name'               => 'item_location',
            'class'              => 'postform',
            'tab_index'          => 10,
            'depth'              => 2,
            'hierarchical'       => 1,
            'taxonomy'           => 'location',
            'hide_empty'      => false ); 

    ?>

<!-- EDIT FORM -->

    <form id="edit_post" name="edit_post" method="post" action="" enctype="multipart/form-data">

        <!-- post name -->
        <fieldset name="item_title">
            <label for="item_title">Item title:</label><br />
            <input type="text" id="item_title" value="<?php echo $post_to_edit->post_title; ?>" tabindex="5" name="item_title" />
        </fieldset>

        <!-- post Content -->
        <fieldset class="item_description">
            <label for="item_description">Item description:</label><br />
            <textarea id="item_description"  tabindex="15" name="item_description"><?php echo $post_to_edit->post_content; ?></textarea>
        </fieldset>

        <!-- post Category -->
        <fieldset id="item_category">
            <label for="item_category">Item category:</label>
            <?php wp_dropdown_categories( $args_cat ); ?>
        </fieldset>

        <!-- post Location -->
        <fieldset id="item_location">
            <label for="item_location">Item location:</label>
            <?php wp_dropdown_categories( $args_loc ); ?>
        </fieldset>

        <!-- custom fields -->

        <fieldset class="item_location2">
            <label for="item_location2">Location 2:</label><br />
            <input type="text" value="<?php echo get_post_meta($post_to_edit->ID,'item_location2', true); ?>" id="item_location2" tabindex="20" name="item_location2" />
        </fieldset>

        <!-- submit button -->
        <fieldset class="submit">
            <input type="submit" value="Save Post" tabindex="40" id="submit" name="submit" />
        </fieldset>

        <input type="hidden" name="postid" value="<?php echo $post_to_edit->ID; ?>" /> <!-- DONT REMOVE OR CHANGE -->
        <input type="hidden" name="action" value="edit_post" />                           <!-- DONT REMOVE OR CHANGE -->
        <input type="hidden" name="change_cat" value="" />                                <!-- DONT REMOVE OR CHANGE -->
    <?php // wp_nonce_field( 'new-post' ); ?>
    </form>
    <!-- END OF FORM -->

<?php } ?><!-- user is logged in -->
<?php } ?><!-- getForm -->

An Edit button was added to the footer of each entry generated by a loop with this form:

<form class="edit-post" action="<?php echo home_url( '/edit'); ?>" method="post">
    <input type="hidden" name="postid" value="<?php the_ID(); ?>" />
    <input type="submit"  value="Edit" />
</form>
  • Which plugin you tried? . And do you have space after end of php tag ? >. If there is any space remove it. It will work for you. I have released a plugin before. So i am interested to know about the plugin name. – Kvvaradha Dec 13 '15 at 12:04
  • @kvvaradha This is my own unofficial plugin and it works, except the warning, that is not because of the php tag (I solved this, thanks!). I am not interested in an official plugin because anyway I must adapt them to my needs. Making my own plugin I can learn something new and I can control what happens. – Iurie Malai Dec 13 '15 at 12:34
1

From the code it seems like your warning comes from doing the redirect too late. redirects should be done, as a rule of thumb, not later then the init action. And after the redirect you should die() (I don't think the wp_redirect does it for you)

As for security, it is not enough to check that the user is logged-in, you need to check if he has the capability to edit the post, something like if current_user_can('edit_post',$post_id). You need to check it both on the UI side and server side. just because you do not show the ability to the hacker doesn't mean he will not construct a special HTTP request to change the post if you don't have protection on the server side.

  • I temporarily commented the wp_redirect(), as I can't find the right place to put it. Not the best solution, but now I don't have this warning. Thanks for your suggestion about capability checking, I will implement it. – Iurie Malai Dec 13 '15 at 18:34
  • in general the way forms are supposed to work, you don't submit them to the page that generates the HTML, but to another URL and from there you redirect back to the page. This create separation of the code handling the submition and the code generating the HTML. For inspiration you can look at how the wordpress admin forms work. – Mark Kaplun Dec 13 '15 at 18:54
0

After some learning and research I abandoned the approach described in my question, indeed I figured out how to add the front end editing functionality using the @TheDeadMedic solution. Now I have only two little questions:

1) Is there a better way to use/set the post ID and the post date? I used for this two hidden inputs: foo_id and foo_date.

2) How secure is the communication between forms?

This is the form that gets the post ID into the "postid" variable and pass it to the "post-form" page:

<form action="<?php echo home_url( '/post-form'); ?>" method="post">
    <input type="hidden" name="postid" value="<?php the_ID(); ?>" /> 
    <input type="submit"  value="Edit" />
</form>

<?php

This is the front end form for posting an editing posts (code added by me is marked with // MY CODE):

class WPSE_Submit_From_Front {
    const NONCE_VALUE = 'front_end_new_post';
    const NONCE_FIELD = 'fenp_nonce';

    protected $pluginPath;
    protected $pluginUrl;
    protected $errors = array();
    protected $data = array();

    function __construct() {
        $this->pluginPath = plugin_dir_path( __file__ );
        $this->pluginUrl  = plugins_url( '', __file__ );

        add_action( 'wp_enqueue_scripts', array( $this, 'addStyles' ) );
        add_shortcode( 'post_from_front', array( $this, 'shortcode' ) );

        // Listen for the form submit & process before headers output
        add_action( 'template_redirect',  array( $this, 'handleForm' ) );
    }

    function addStyles() {
        wp_enqueue_style( 'submitform-style', "$this->pluginUrl/submitfromfront.css" );
    }

    /**
     * Shortcodes should return data, NOT echo it.
     *
     * @return string
     */
    function shortcode() {
        if ( ! current_user_can( 'publish_posts' ) )
            return sprintf( '<p>Please <a href="%s">login</a> to post links.</p>', esc_url( wp_login_url(  get_permalink() ) ) );
        elseif ( $this->isFormSuccess() )
            return '<p class="success">Nice one, post created.</p>';
        else
            return $this->getForm();
    }

    /**
     * Process the form and redirect if sucessful.
     */
    function handleForm() {
        if ( ! $this->isFormSubmitted() )
            return false;

        // http://php.net/manual/en/function.filter-input-array.php
        $data = filter_input_array( INPUT_POST, array(
            // MY CODE
            'foo_id'      => FILTER_DEFAULT,
            'foo_date'    => FILTER_DEFAULT,
            // END MY CODE
            'postTitle'   => FILTER_DEFAULT,
            'postContent' => FILTER_DEFAULT,
            'location2'   => FILTER_DEFAULT,
        ));

        $data = wp_unslash( $data );
        $data = array_map( 'trim', $data );

        // You might also want to more aggressively sanitize these fields
        // By default WordPress will handle it pretty well, based on the current user's "unfiltered_html" capability

        $data['postTitle']   = sanitize_text_field( $data['postTitle'] );
        $data['postContent'] = wp_check_invalid_utf8( $data['postContent'] );
        $data['location2']   = sanitize_text_field( $data['location2'] );

        $this->data = $data;

        if ( ! $this->isNonceValid() )
            $this->errors[] = 'Security check failed, please try again.';

        if ( ! $data['postTitle'] )
            $this->errors[] = 'Please enter a title.';

        if ( ! $data['postContent'] )
            $this->errors[] = 'Please enter the content.';

        if ( ! $this->errors ) {
            $post_id = wp_insert_post( array(
                // MY CODE
                'ID'           => $data['foo_id'],
                'post_date'    => $data['foo_date'],
                // END MY CODE
                'post_title'   => $data['postTitle'],
                'post_content' => $data['postContent'],
                'post_status'  => 'publish',
            ));

            if ( $post_id ) {
                add_post_meta( $post_id, 'location2', $data['location2'] );

                // Redirect to avoid duplicate form submissions
                wp_redirect( add_query_arg( 'success', 'true' ) );
                exit;

            } else {
                $this->errors[] = 'Whoops, please try again.';
            }
        }
    }

    /**
     * Use output buffering to *return* the form HTML, not echo it.
     *
     * @return string
     */
    function getForm() {

    // MY CODE
    if( 'POST' == $_SERVER['REQUEST_METHOD'] && isset( $_POST['postid'] ) ) {
        $post_to_edit                   = array();
        $post_to_edit                   = get_post( $_POST['postid'] );
        $this->data['foo_id']           = $post_to_edit->ID;
        $this->data['foo_date']         = $post_to_edit->post_date;
        $this->data['item_name']        = $post_to_edit->post_title;
        $this->data['item_description'] = $post_to_edit->post_content;
    }
    // END MY CODE

        ob_start();
        ?>

<div id ="frontpostform">
    <?php foreach ( $this->errors as $error ) : ?>

        <p class="error"><?php echo $error ?></p>

    <?php endforeach ?>

    <form id="formpost" method="post">
        <fieldset>
            <label for="postTitle">Post Title</label>
            <input type="text" name="postTitle" id="postTitle" value="<?php

                // "Sticky" field, will keep value from last POST if there were errors
                if ( isset( $this->data['postTitle'] ) )
                    echo esc_attr( $this->data['postTitle'] );

            ?>" />
        </fieldset>

        <fieldset>
            <label for="postContent">Content</label>
            <textarea name="postContent" id="postContent" rows="10" cols="35" ><?php

                if ( isset( $this->data['postContent'] ) )
                    echo esc_textarea( $this->data['postContent'] );

            ?></textarea>
        </fieldset>

        <fieldset>
        <label for="location2">Location 2</label>
        <input type="text" name="location2" id="location2" title="Location 2 (opțional)" value="<?php

            // "Sticky" field, will keep value from last POST if there were errors
            if ( isset( $this->data['location2'] ) )
                echo esc_attr( $this->data['location2'] );

        ?>" />
        </fieldset>

        <fieldset>
            // MY CODE
            <input type="hidden" name="foo_id" id="foo_id" value="<?php

            // "Sticky" field, will keep value from last POST if there were errors
            if ( isset( $this->data['foo_id'] ) )
                echo esc_attr( $this->data['foo_id'] );

        ?>" />

            <input type="hidden" name="foo_date" id="foo_date" value="<?php

            // "Sticky" field, will keep value from last POST if there were errors
            if ( isset( $this->data['foo_date'] ) )
                echo esc_attr( $this->data['foo_date'] );

        ?>" />
            // END MY CODE
            <button type="submit" name="submitForm" >Create Post</button>
        </fieldset>

        <?php wp_nonce_field( self::NONCE_VALUE , self::NONCE_FIELD ) ?>
    </form>
</div>

        <?php
        return ob_get_clean();
    }

    /**
     * Has the form been submitted?
     *
     * @return bool
     */
    function isFormSubmitted() {
        return isset( $_POST['submitForm'] );
    }

    /**
     * Has the form been successfully processed?
     *
     * @return bool
     */
    function isFormSuccess() {
        return filter_input( INPUT_GET, 'success' ) === 'true';
    }

    /**
     * Is the nonce field valid?
     *
     * @return bool
     */
    function isNonceValid() {
        return isset( $_POST[ self::NONCE_FIELD ] ) && wp_verify_nonce( $_POST[ self::NONCE_FIELD ], self::NONCE_VALUE );
    }
}

new WPSE_Submit_From_Front;

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.