# Content Protection

> Gate the rendering of any element (widget / section / container) by either user role allowlist or password protection. Hides the element from unauthorized viewers, or shows a password-entry form.

**Class file:** `includes/Extensions/Content_Protection.php` (1,015 lines)
**Slug:** `content-protection` (`config.php` `extensions` key)
**Public docs:** <https://essential-addons.com/elementor/docs/content-protection/>
**Pro-shared:** ✅ Pro-only (Lite has Promotion teaser)

## Overview

Adds a "Content Protection" panel under Section / Container / common Style tab. Two protection types:

1. **User role** — render only when current user has one of the selected roles
2. **Password protected** — render only after the visitor enters the correct password (form submission tracked via cookie / nonce)

Hooks `should_render` to short-circuit section/container rendering and `render_content` to filter widget HTML.

## Features

- **Role-based gating** — multi-select role allowlist
- **Password gating** — single password per element; visitor enters once, gets cookied for the rest of the session
- **Styled password form** — full Group_Control_Border / Box_Shadow / Typography for the form fields and submit button
- **Submit button text customization**
- **Placeholder text customization**

## Pro vs Lite

| Capability | Lite | Pro |
| --- | --- | --- |
| Panel visible in editor | Teaser only | ✅ Full panel |
| Role / password gating | ❌ | ✅ |
| Frontend password form | ❌ | ✅ |

## File Map

| File | Role |
| --- | --- |
| `includes/Extensions/Content_Protection.php` | Class — controls + render gates |
| `assets/front-end/css/view/protected-content.min.css` | Styles the password form |
| `config.php` (extensions key) | Includes single CSS dep on `protected-content.min.css` (`'context' => 'view'`) |

No JS file — password form is a standard HTML form posting back to the page.

## Architecture

Constructor wires 6 hooks (`Content_Protection.php:22-26`):

```php
add_action( 'elementor/element/common/_section_style/after_section_end',     [ $this, 'register_controls' ], 10 );
add_action( 'elementor/element/section/section_advanced/after_section_end', [ $this, 'register_controls' ] );
add_action( 'elementor/element/container/section_layout/after_section_end',  [ $this, 'register_controls' ] );
add_action( 'elementor/widget/render_content',         [ $this, 'render_content' ], 10, 2 );
add_filter( 'elementor/frontend/section/should_render',   [ $this, 'render_content' ], 10, 2 );
add_filter( 'elementor/frontend/container/should_render', [ $this, 'render_content' ], 9, 2 );
```

The single `render_content` method serves both `render_content` (filter on widget HTML) and `should_render` (filter on section/container boolean). It detects context by argument type:

- For widgets: returns modified HTML string (replace content with password form / empty)
- For sections / containers: returns `false` to skip rendering, or `true` to allow

**Priority asymmetry:** container uses priority `9` while section uses `10`. Likely intentional — container renders earlier so Pro's gate fires before some other Pro extension. Verify when editing.

## Controls Reference

| Control id | Purpose |
| --- | --- |
| `eael_ext_content_protection` | Master switch (`'yes' \| 'no'`) |
| `eael_ext_content_protection_type` | `'role' \| 'password'` |
| `eael_ext_content_protection_role` | Multi-select role allowlist (when type = role) |
| `eael_ext_content_protection_password` | Password value (stored in element settings — see "Behavior Quirks") |
| `eael_ext_content_protection_password_placeholder` | Form placeholder text |
| `eael_ext_content_protection_password_submit_btn_txt` | Submit button label |
| `eael_ext_content_protection_submit_button_color` | Form button text color |
| `eael_ext_content_protection_submit_button_bg_color` | Form button background |
| `Group_Control_Border eael_ext_content_protection_submit_button_border` | Submit button border |
| `Group_Control_Box_Shadow eael_ext_content_protection_submit_button_shadow` | Submit button shadow |
| `Group_Control_Typography eael_ext_content_protection_text` | Password form field typography |

## Render Output — password form

When type = `password` and visitor hasn't authenticated, the protected element is replaced with:

```html
<div class="eael-password-protected-content">
    <form class="eael-password-protected-content-form" method="post">
        <div class="eael-password-protected-content-fields">
            <input type="password" name="eael_protected_pwd" placeholder="..." class="eael-input" />
            <button type="submit" class="eael-submit">Submit</button>
        </div>
    </form>
</div>
```

On submit, the password is compared to the saved value. On match, a cookie is set and the page reloads with the protected content visible.

## Behavior Flow

1. **Editor:** user picks Content Protection panel on a Section. Enables, picks type (role/password), configures.
2. **Save:** settings persist in `_elementor_data`.
3. **Frontend render:**
   - **Role type:** `current_user_can()` check against allowlist. Pass → render. Fail → skip (section/container) or replace with empty/login prompt (widget).
   - **Password type:** Check cookie. Set → render. Unset → render the password form instead.
4. **Form submit:** POST to same page. Pro compares input against saved password. Match → set cookie + reload. No match → reload with error message.

## Hooks & Filters

### Elementor hooks consumed

| Hook | Method | Purpose |
| --- | --- | --- |
| `elementor/element/{common,section,container}/.../after_section_end` | `register_controls` | Add panel |
| `elementor/widget/render_content` | `render_content` | Filter widget HTML |
| `elementor/frontend/{section,container}/should_render` | `render_content` | Boolean gate |

### Pro / EA hooks emitted

None directly. Form-submit flow is standard WP POST.

## Behavior Quirks

- **Password stored in plain in element settings.** Anyone with access to `_elementor_data` post meta can read it. Treat as obscurity, not security. Don't use for genuinely sensitive content.
- **Per-element password.** Each protected element has its own password and cookie. A user authenticating one section doesn't unlock another.
- **Editor renders all content.** Same as Conditional_Display — editor preview bypasses the gate.
- **No password reset / logout UI.** Once cookied, the user stays authenticated until cookie expires or is manually cleared.
- **Container priority 9 vs section 10.** Containers gate one priority earlier — verify intentional when editing.

## Known Limitations / Gaps

1. **Password plaintext in post meta.** Document in privacy policy when used; never use for high-sensitivity content.
2. **No CSRF on the form submit.** Standard POST without nonce. A cross-origin form can submit the password — but it can't read the response. Low risk; flag for hardening.
3. **No rate limiting on password attempts.** Brute force possible. Add throttle.
4. **Cookie has no `httpOnly` / `secure` flags documented.** Verify cookie set logic.
5. **No fallback content option.** Hidden elements just disappear; "Sorry, you don't have access" content option would be friendlier.

## Code Pointers

- Constructor: `Content_Protection.php:20-27`
- Controls registration: `:29-end of register_controls`
- `render_content` gate: search for `public function render_content`
- Password form HTML: search for `eael-password-protected-content` selector

## Cross-References

- Shared patterns: [`_patterns.md`](_patterns.md) — multi-element registration, render-time gating
- Rule: [`.claude/rules/php-standards.md`](../../.claude/rules/php-standards.md) — capability checks
