2

This is for WordPress, just to make that clear. I'm asking here since I suspect that I need to provide a bounty on this question (worth 400).

I need to add AJAX to my form submit to avoid page reload / page refresh. I have tried several different versions and none of them are working.

The general idea here is to protect parts of a post whereof that part is code wrapped in <pre> and <code> tags.

These tags are from the prismjs highlighter and looks like this:

<pre><code class="language-php">code here</code></pre>

These tags have four different classes;

  • PHP
  • HTML
  • CSS
  • JS

This is why the preg_replace uses the ('/(<pre[^>]*>\s*<code[^>]*>) formatting as it needs to cover (handle) the class added.

Furthermore, a cookie is set so that once the user has provided a correct password, the password is remembered. The user should not have to re-enter the password each time they view a post with protected content.

I have an empty DIV acting as a placeholder for showing messages (success and error). The idea here is to show an error if the user submits an incorrect password. If the password match, show the content (code).

This is the code I am working on:

add_filter( 'the_content', 'wrap_code_in_shortcode' , 9 );
function wrap_code_in_shortcode( $content ) {

    if ( ! in_category( 'premium' ) ) return $content;
    
    $content = preg_replace('/(<pre[^>]*>\s*<code[^>]*>)/',"[protected]$1", $content);

    $content = preg_replace('/(<\/code>\s*<\/pre>)/', "$1[/protected]", $content);

    return $content;
}

add_shortcode( 'protected', 'protected_function' );
function protected_function($atts, $content=null){

    $userpass = isset( $_REQUEST['password']) ? $_REQUEST['password'] : (isset($_COOKIE['userpass']) ? $_COOKIE['userpass'] : NULL );

        if ( in_array( $userpass, array('testpw') ) ) {

            $return_code = do_shortcode( $content );

        } else {

    $return_code = '<div style="margin-top:20px; font-size:15px">Submit password to view.</div>
    
    <div id="errorPWdata"></div>

        <form method="post" onsubmit="protectedFunction(this);">

            <input required style="display: block; width: 69%; height: 50px; margin-right: 1%; float: left; border: 2px solid #333;" type="text" placeholder="&#32;Password Here" name="password" id="userpass">

            <input style="display: block; margin: 0px; width: 30%; height: 50px; background-color: #333; color: #fff;" id="protectedButton" type="submit" value="Submit">

        </form>';
    
        ?>
    <script>

        function protectedFunction(form) {
        $('#protectedButton').on( 'click', function(e) {
        e.preventDefault();
        $.ajax({
                success: function(data) {
                    $("#errorPWdata").html(data);
                },
                error: function() {
                    alert("Password record error. Contact the administrator.");
                }
            });
        document.cookie = "userpass=" + escape(form.userpass.value) + "; path=/";
    }
}
    </script>
    <?php
}

    return $return_code;

}
  • Can you share your AJAX WordPress function callback, what is the target of the `$.ajax` function? – Plamen Nikolov Mar 15 '21 at 16:06
  • Do you want code for the ajax handler which returns protected content only? – Intelligent Web Solution Mar 15 '21 at 17:02
  • @Chengmin I need to avoid page reload, hence the AJAX. If the password is correct, show the code without reloading (all parts of the content that are protected will be "unlocked"). If the password is not correct, display an error message below the input field. –  Mar 15 '21 at 17:14
  • @PlamenNikolov I don't have a complete working AJAX code. This is what I need help with. –  Mar 15 '21 at 17:15
  • @HaroldAldersen it is not clear exactly what do you need. To password protect a post content without page reload is not complex. But in order to do so, can you specify your content setup - e.g where will be the `[protected]` short-code be placed? – Plamen Nikolov Mar 15 '21 at 17:25
  • @PlamenNikolov It's done automatically using the_content filter hook. If you look at the code in my question, you'll see, –  Mar 15 '21 at 18:46
  • @PlamenNikolov it is wrapping `
    ` and `` tags automatically. `$content = preg_replace('/(
    ]*>\s*]*>)/',"[protected]$1", $content);
    
        $content = preg_replace('/(<\/code>\s*<\/pre>)/', "$1[/protected]", $content);`
    –  Mar 15 '21 at 18:47
  • @HaroldAldersen I wrote the solution below. Please check it and feel free to reach me if you found any issues. – Intelligent Web Solution Mar 16 '21 at 01:43

2 Answers2

4

I learned to details your code snippet and notice mistakes. Please, read everything firstly, then you could try.

Mistake 1

Shortcode has been defined with a wrong function name

add_shortcode( 'protected', 'shortcode_protected' );

Should be replaced with

add_shortcode( 'protected', 'bio_protected_function' );

Mistake 2

Script tag should be replaced by the enqueueing system to guarantee script sequences.

The snippet

<script>
    function protectedFunction(form) {
        $('#protectedButton').on('click', function (e) {
            e.preventDefault();
            $.ajax({
                success: function (data) {
                    $("#errorPWdata").html(data);
                },
                error: function () {
                    alert("Password record error. Contact the administrator.");
                }
            });
            document.cookie = "userpass=" + escape(form.userpass.value) + "; path=/";
        }
    }
</script>

Should be replaced with this one

add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'bio-shortcode-protected',
        get_theme_file_uri( 'assets/js/bio-shortcode-protected.js' ),
        [ 'jquery' ],
        '1',
        true
    );
} );
wp_enqueue_scripts();

An appropriate bio-shortcode-protected.js file should be created path-to-your-theme/assets/js/bio-shortcode-protected.js (Content of the file in next mistake)

Mistake 3

  1. By default WordPress loads jQuery as a global JavaScript object

    jQuery should works meanwhile $ probably won't. The script should start with jQuery wrapper to guarantee $ alias working.

  2. Provided Script has incorrect syntax. There should be a close parentese symbol )

  3. Better to use submit handler instead of click handler. I simplified your handler by handling submit instead of click. Clicking input button triggers submit and click handler not required.

Finally, the bio-shortcode-protected.js content should be

jQuery(function($){
    const settings = window.bioShortcodeData;

    function init() {
        $('#protectedForm').on( 'submit', function(e) {
            e.preventDefault();

            const form = this;

            $.post({
                url: settings['endpoint'],
                data: {
                    'action': settings['action'],
                    'bio-post-id': settings['post-id'],
                    'nonce': settings['nonce'],
                    'password': form.userpass.value,
                },
                success: function(data) {
                    if (!data.status) {
                        alert(data.message);

                        $('#errorPWdata')
                            .empty()
                            .append(data.message);

                        return;
                    }

                    if (data.isValidPassword) {
                        document.cookie = "userpass=" + escape(form.userpass.value) + "; path=/";
                    }

                    $('#bio-protected-content').replaceWith(data.content);
                    init() // for reattach submission handler
                },
                error: function() {
                    alert("Server error");
                }
            });

        })
    }

    init();
})

And appropriate little bit improved shortcode template should look like:

function bio_protected_function( $atts, $content = null ) {
    add_action( 'wp_enqueue_scripts', function () {
        wp_enqueue_script(
            'bio-shortcode-protected',
            get_theme_file_uri( 'assets/js/bio-shortcode-protected.js' ),
            [ 'jquery' ],
            '1',
            true
        );
        wp_localize_script(
            'bio-shortcode-protected',
            'bioShortcodeData',
            [
                'post-id'  => get_the_ID(),
                'nonce'    => wp_create_nonce( 'bio-shortcode' ),
                'endpoint' => admin_url( 'admin-ajax.php' ),
                'action'   => 'bio_protected_code'
            ]
        );
    } );
    wp_enqueue_scripts();

    if ( bio_validate_the_password() ) {
        return do_shortcode( $content );
    }

    ob_start();
    ?>
    <div class="bio-protected-content" id="bio-protected-content">
        
        <div style="margin-top:20px; font-size:15px">Submit password to view.</div>

        <div id="errorPWdata"></div>

        <form method="post" id="protectedForm">

            <input required
                   style="display: block; width: 69%; height: 50px; margin-right: 1%; float: left; border: 2px solid #333;"
                   type="text" placeholder="&#32;Password Here" name="password" id="userpass">

            <input style="display: block; margin: 0px; width: 30%; height: 50px; background-color: #333; color: #fff;"
                   id="protectedButton" type="submit" value="Submit">

        </form>
    </div>
    <?php

    return ob_get_clean();
}

Mistake 4

If the form will be submitted what would heppend - is WordPress starts to build content of the page, firstly header, the content (the_content filter), then footer. And checking password inside shortcode not so good idea. This way sending unnecessary page chunks. The only thing need to fetch by AJAX is clean apropriate post content.

This is where ajax-endpoint comes into play. Endpoint should be created by placing the snippet to functions.php file:

add_action( 'wp_ajax_nopriv_bio_protected_code', 'bio_protected_code_endpoint' );
add_action( 'wp_ajax_bio_protected_code', 'bio_protected_code_endpoint' );
function bio_protected_code_endpoint() {
    $is_valid_nonce = wp_verify_nonce( $_REQUEST['nonce'], 'bio-shortcode' );
    if ( ! $is_valid_nonce ) {
        wp_send_json(
            [
                'status'  => false,
                'message' => 'Invalid nonce',
            ]
        );
        exit;
    }

    $post_id = $_REQUEST['bio-post-id'];

    $post_type           = get_post_type( $post_id );
    $available_post_type = 'your-post-type';
    if ( $available_post_type !== $post_type ) {
        wp_send_json(
            [
                'status'  => false,
                'message' => 'Not available post type',
            ]
        );
        exit;
    }

    global $post;

    $post    = get_post( $post_id );
    $content = apply_filters( 'the_content', $post->post_content );

    wp_send_json(
        [
            'status'          => true,
            'isValidPassword' => bio_validate_the_password(),
            'content'         => $content
        ]
    );

    exit;
}

And password validation function

function bio_validate_the_password() {
    $userpass = $_REQUEST['password'] ?? $_COOKIE['userpass'] ?? null;

    return in_array( $userpass, [ 'testpw' ] );
}

Last words:

I don't recommend set password in Cookie. It's potential security breach. Better to set Session password variable. But this way not much better.

There is a good answer why not to store password in Session and what to do instead. Is it secure to store a password in a session?

123
  • 2,169
  • 3
  • 11
  • 35
  • Unfortunately, this does not work no matter what theme I use. All I get is a console log alert. –  May 07 '21 at 08:43
0

Please try with below code.

Shortcode handling part:

// add [protected] shortcode to content.
add_filter( 'the_content', 'wrap_code_in_shortcode' , 9 );
function wrap_code_in_shortcode( $content ) {
    if ( ! in_category( 'premium' ) ) return $content;
    $content = preg_replace('/(<pre[^>]*>\s*<code[^>]*>)/',"[protected]$1", $content);
    $content = preg_replace('/(<\/code>\s*<\/pre>)/', "$1[/protected]", $content);
    return $content;
}

// function to check if password is valid
function is_user_password_valid($userpass, $post_id) {
    return in_array( $userpass, array('test') );
}

// define shortcode. you can combine(merge) this section with wrap_code_in_shortcode()
add_shortcode( 'protected', 'shortcode_protected' );
function shortcode_protected($atts, $content=null){
    $userpass = isset( $_REQUEST['password']) ? $_REQUEST['password'] : (isset($_COOKIE['userpass']) ? $_COOKIE['userpass'] : NULL );
    $post_id = get_the_ID();

    if ( is_user_password_valid($userpass, $post_id) ) {
        $return_code = do_shortcode( $content );
    } else {
        $return_code = 
        '<div id="protected-area">
            <div style="margin-top:20px; font-size:15px">Submit password to view.</div>
            <form method="post" onsubmit="getProtectedContent(event);">
                <input required type="text" placeholder="&#32;Password Here" name="password">
                <input type="submit" value="Submit">
            </form>
        </div><!-- end of #protected-area -->';
        $return_code .= '<script>
            function getProtectedContent(event) {
                event.preventDefault();
                let password = event.target.elements.password.value;
                jQuery.ajax({
                    url: "' . admin_url('admin-ajax.php') . '" ,
                    method: "POST",
                    data: {
                        action: "get_protected_content",
                        post_id: ' . $post_id . ',
                        password
                    },
                    success: function(result) {
                        if (result.success) {
                            jQuery("#protected-area").html(result.data);
                            document.cookie = "userpass=" + password + "; path=/";
                        } else {
                            alert(result.data);
                        }
                    },
                    error: function() {
                        alert("Something is wrong.");
                    }
                });
                return false;
            }
        </script>';
    }

    return $return_code;
}

Ajax Request Handling Part:

add_action( 'wp_ajax_get_protected_content', 'get_protected_content' );
add_action( 'wp_ajax_nopriv_get_protected_content', 'get_protected_content' );
function get_protected_content() {
    // validation
    if ( empty( $_POST['post_id'] ) ) {
        wp_send_json_error( 'Wrong Post ID' );
    }
    $post_id = (int)$_POST['post_id'];

    if ( empty( $_POST['password'] ) || ! is_user_password_valid( $_POST['password'], $post_id ) ) {
        wp_send_json_error( 'Wrong Password' );
    }
    
    $content = get_post_field( 'post_content', $post_id );
    // var_dump($content);
    if ( preg_match( '/(<pre[^>]*>\s*<code[^>]*>.*<\/code>\s*<\/pre>)/', $content, $matches ) ) {
        wp_send_json_success( apply_filters( 'the_content', $matches[1] ) );
    } else {
        wp_send_json_error( 'No Protected Content Found' );
    }
}

Hope this code help you.

  • It gives the correct error if providing the incorrect password. Problem is when giving the correct password, it just says "No Protected Content Found". –  Mar 16 '21 at 04:48
  • I copied the code you posted. it does not work. Either you post a solution or someone else will. No Skype. –  Mar 16 '21 at 05:05
  • It refers to $matches which cannot be found anywhere. –  Mar 16 '21 at 05:05
  • You can debug the issue by checking the value of `$content` and regular expression. Please attach the page content so that I can check what the problem is. – Intelligent Web Solution Mar 16 '21 at 06:26
  • There's nothing to debug. Your code have argument references that do not exist and instead of inline success / error messages you have added alerts (pop-ups). –  Mar 16 '21 at 07:08
  • I think you didn't check `preg_match` function at all. Please check https://www.php.net/manual/en/function.preg-match.php – Intelligent Web Solution Mar 16 '21 at 08:22
  • How can that be the problem? The wrapper has worked up until you changed the code. If I use the old version of this everything works. All I need is the ajax to prevent page reload. –  Mar 17 '21 at 11:12