=== RIACO Content Protector ===
Contributors: prototipo88
Tags: content protection, password, password protection, hide content, restrict content
Requires at least: 6.2
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 1.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Protect any portion of your WordPress content using a simple shortcode. Includes global password, AJAX unlock, and site-wide instant access.

== Description ==

**RIACO Content Protector** allows you to protect *any part* of your WordPress content using a shortcode.  
Unlike the built-in post password protection, this plugin protects only what you wrap, *not the whole post*.

Perfect for:

* Protecting premium blocks of content  
* Protecting guides, downloads, links, or sensitive sections   
* Paywall-style snippets

### Features

* Protect **only specific content** inside posts/pages
* Uses a minimal shortcode:
  `[riaco_content_protector] Hidden text here [/riaco_content_protector]`
* **Global password** stored in plain text, like WordPress page passwords.
* AJAX-based form — no page reload
* Unlocks **all protected sections** on the site after correct password
* Optional cookie persistence (remember unlocked content for a configurable number of days)
* Secure implementation using nonces, hashed tokens, and transients
* Developer-friendly: filters, actions, and JS custom events at every key point in the unlock flow

**Important:**

* The global password is stored in plain text, just like WordPress page passwords. It can be read by user with 'manage_options' ability.
* If the global password or "Remember Unlocked" duration is changed in settings, all existing unlock cookies are invalidated. Users will need to re-enter the new password to access protected content.

### How It Works

Wrap content you want to protect:

`
[riaco_content_protector]
This text will be hidden until the visitor enters the password.
[/riaco_content_protector]
`

Set the global password under:

**Settings > Content Protector**

Visitors will see a modern, styled form.  
After entering the correct password:

* The content unlocks immediately  
* All other protected areas unlock automatically  
* An optional cookie can keep everything unlocked for a chosen number of days  

### Security

* Nonces on every request  
* Secure HMAC token for cookie authentication  
* Sanitized shortcode attributes  
* Escaped output  
* No sensitive data stored in cookies  
* Global password stored in plain text, like WordPress page passwords. 

### Cookie

The default cookie name is `riaco_cp_unlocked_global`. The scope suffix `global` can be changed via the `riaco_cp_cookie_scope` filter, which also changes the cookie name accordingly.

### Style

You can style the content protector box.

It has this class: `.riaco-cp--container`, so you can add in your `style.css`:

`
.riaco-cp--container {
    background: #f8f9fa;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 6px;
}
`

You can replace button classes using:

`
add_filter( 'riaco_cp_button_classes', function( $classes ) {
    return 'button my-custom-button-class';
});
`

Or you can remove button classes:

`
add_filter( 'riaco_cp_button_classes', function( $classes ) {
    return str_replace( 'wp-element-button', '', $classes );
});
`

=== For Developers ===

The plugin exposes actions and filters at every key point so you can extend or customise behaviour without modifying plugin files.

**Filters**

`riaco_cp_default_options` — modify the option defaults written on first activation.

`
add_filter( 'riaco_cp_default_options', function( $defaults ) {
    $defaults['remember_days'] = 30;
    return $defaults;
});
`

`riaco_cp_instance_id` — override the computed instance ID for a protected block.

`
add_filter( 'riaco_cp_instance_id', function( $id, $atts, $content ) {
    return ! empty( $atts['id'] ) ? 'my_prefix_' . $atts['id'] : $id;
}, 10, 3 );
`

`riaco_cp_transient_expiry` — change how long protected content is cached (default: `DAY_IN_SECONDS`).

`
add_filter( 'riaco_cp_transient_expiry', function( $seconds, $instance_id ) {
    return HOUR_IN_SECONDS * 6;
}, 10, 2 );
`

`riaco_cp_lock_message` — customise the message shown above the password field.

`
add_filter( 'riaco_cp_lock_message', function( $message, $instance_id ) {
    return __( 'Members only. Enter your access code:', 'my-theme' );
}, 10, 2 );
`

`riaco_cp_unlocked_content` — wrap or replace the HTML around unlocked content.

`
add_filter( 'riaco_cp_unlocked_content', function( $html, $content, $instance_id ) {
    return '<div class="my-unlocked-wrapper">' . $content . '</div>';
}, 10, 3 );
`

`riaco_cp_cookie_args` — modify the options passed to `setcookie()`.

`
add_filter( 'riaco_cp_cookie_args', function( $args, $cookie_name ) {
    $args['samesite'] = 'Strict';
    return $args;
}, 10, 2 );
`

`riaco_cp_max_attempts` — change the brute-force rate-limit threshold (default: 5).

`
add_filter( 'riaco_cp_max_attempts', function( $max ) {
    return 10;
});
`

`riaco_cp_rate_limit_window` — change the rate-limit window in seconds (default: 15 minutes).

`
add_filter( 'riaco_cp_rate_limit_window', function( $seconds ) {
    return 5 * MINUTE_IN_SECONDS;
});
`

`riaco_cp_content_access` — override the cookie check before protected content is rendered. Return `null` to use the default cookie check, `true` to force-grant access, or `false` to force the lock form.

`
add_filter( 'riaco_cp_content_access', function( $access, $instance_id, $atts ) {
    // Grant access to logged-in users automatically.
    if ( is_user_logged_in() ) {
        return true;
    }
    return null; // Fall through to cookie check for guests.
}, 10, 3 );
`

`riaco_cp_locked_html` — replace or wrap the entire locked form HTML.

`
add_filter( 'riaco_cp_locked_html', function( $html, $instance_id, $atts ) {
    return '<div class="my-paywall">Subscribe to see this content.</div>';
}, 10, 3 );
`

`riaco_cp_validate_password` — override password validation. Return `null` to use the default `hash_equals` check, `true` to grant access, or `false` to deny. When non-null, the global password is not consulted.

`
add_filter( 'riaco_cp_validate_password', function( $result, $user_pass, $instance_id ) {
    // Accept a secondary password for a specific block.
    if ( $instance_id === 'riaco_cp_vip' && $user_pass === 'vip-secret' ) {
        return true;
    }
    return null; // Defer to global password check.
}, 10, 3 );
`

`riaco_cp_rate_limit_bypass` — bypass the brute-force counter for trusted IP addresses.

`
add_filter( 'riaco_cp_rate_limit_bypass', function( $bypass, $ip_address ) {
    $trusted = array( '127.0.0.1', '192.168.1.100' );
    return in_array( $ip_address, $trusted, true );
}, 10, 2 );
`

`riaco_cp_cookie_scope` — change the cookie scope suffix (default: `'global'`). The cookie name becomes `riaco_cp_unlocked_{scope}`. Use this to implement per-post or per-instance independent lock states.

`
add_filter( 'riaco_cp_cookie_scope', function( $scope, $instance_id ) {
    // Use a per-post cookie so each post unlocks independently.
    return 'post_' . get_the_ID();
}, 10, 2 );
`

`riaco_cp_ajax_response` — inject additional fields into the JSON success response sent after unlock.

`
add_filter( 'riaco_cp_ajax_response', function( $data, $instance_id ) {
    $data['redirect'] = home_url( '/members/' );
    return $data;
}, 10, 2 );
`

`riaco_cp_js_data` — add extra data to the `RIACO_CP_Ajax` JavaScript object.

`
add_filter( 'riaco_cp_js_data', function( $data ) {
    $data['my_feature_enabled'] = true;
    return $data;
});
`

**Actions**

`riaco_cp_loaded` — fires after the plugin initialises; receives the `Plugin` instance.

`
add_action( 'riaco_cp_loaded', function( $plugin ) {
    // $plugin->settings, $plugin->shortcode, etc. are available here.
});
`

`riaco_cp_form_fields` — inject extra hidden inputs or markup inside the password form.

`
add_action( 'riaco_cp_form_fields', function( $instance_id ) {
    echo '<input type="hidden" name="my_field" value="' . esc_attr( $instance_id ) . '">';
});
`

`riaco_cp_password_correct` — fires when a visitor enters the correct password.

`
add_action( 'riaco_cp_password_correct', function( $instance_id ) {
    // Log unlock event, fire analytics, etc.
});
`

`riaco_cp_password_incorrect` — fires on a failed attempt; `$attempts` is the running total for this IP.

`
add_action( 'riaco_cp_password_incorrect', function( $instance_id, $attempts ) {
    if ( $attempts >= 3 ) {
        // Send alert, block IP in your firewall, etc.
    }
}, 10, 2 );
`

`riaco_cp_cookie_set` — fires after the unlock cookie is written.

`
add_action( 'riaco_cp_cookie_set', function( $cookie_name, $expire ) {
    // Record unlock timestamp, sync to a log, etc.
}, 10, 2 );
`

`riaco_cp_settings_page_after` — fires after the free plugin's settings form. Use it to render your own `<form>` block with additional settings fields below the existing form.

`
add_action( 'riaco_cp_settings_page_after', function() {
    // Render a separate settings form here.
});
`

**JavaScript Custom Events**

The plugin triggers jQuery custom events on the `.riaco-cp--container` element at key moments. Listen from any JavaScript file that loads after the plugin's script.

`riaco_cp:before_submit` — fires just before the AJAX password call. Call `e.preventDefault()` to cancel the submission (the UI is restored automatically).

`
$(document).on('riaco_cp:before_submit', '.riaco-cp--container', function(e, instanceId, password) {
    // e.preventDefault(); // uncomment to cancel submission
});
`

`riaco_cp:unlock_success` — fires after a successful AJAX response, before the container is replaced with unlocked content.

`
$(document).on('riaco_cp:unlock_success', '.riaco-cp--container', function(e, instanceId, responseData) {
    console.log('Unlocked:', instanceId, responseData);
});
`

`riaco_cp:unlock_error` — fires on wrong password, rate-limit hit, or network/server failure.

`
$(document).on('riaco_cp:unlock_error', '.riaco-cp--container', function(e, instanceId, errorMessage) {
    console.warn('Unlock failed for', instanceId, errorMessage);
});
`

== Installation ==

1. Upload the plugin folder to `/wp-content/plugins/`
2. Activate the plugin through **Plugins > Installed Plugins**
3. Go to **Settings > Content Protector** and configure your global password
4. Add the shortcode to any post or page

`
[riaco_content_protector] This is hidden. [/riaco_content_protector]
`

== Frequently Asked Questions ==

= Can I protect multiple sections on the same page? =  
Yes. All instances use the same global password and unlock together.

= Does this protect the entire post? =  
No — only the content wrapped in the shortcode.

= Are passwords hashed in the database? =
No, the global password is stored in plain text like WordPress page passwords for easy admin management.

= Does this work with Gutenberg / block editor? =  
Yes. It works in both Classic and Block Editor.  
You can insert the shortcode inside Paragraph block or using Shortcode block.

= What happens when I change the global password? =
All previously unlocked content cookies are invalidated. Users must re-enter the new password.

== Screenshots ==

1. Frontend password form  
2. Protected content example  
3. Settings page with global password option  

== Changelog ==

= 1.1.0 =
* Added `riaco_cp_content_access` filter to override the cookie check (role/subscription bypass)
* Added `riaco_cp_locked_html` filter to replace the full locked-form HTML
* Added `riaco_cp_validate_password` filter for custom password validation (per-post passwords, etc.)
* Added `riaco_cp_rate_limit_bypass` filter to exempt trusted IPs from the brute-force counter
* Added `riaco_cp_cookie_scope` filter for per-post or per-instance independent lock state
* Added `riaco_cp_ajax_response` filter to inject extra fields into the unlock JSON response
* Added `riaco_cp_js_data` filter to extend the `RIACO_CP_Ajax` JavaScript object
* Added `riaco_cp_settings_page_after` action for extensions to add their own settings form
* Added JS custom events: `riaco_cp:before_submit`, `riaco_cp:unlock_success`, `riaco_cp:unlock_error`
* Added admin footer review prompt on the settings page

= 1.0.0 =
* Initial release
* Shortcode protection
* Global password
* AJAX unlock
* Cookie remember feature
* Automatic unlock of all instances

== License ==

This plugin is licensed under GPLv2 or later.
