Troubleshooting

Security package issues and fixes.

Nonce Issues

"Invalid nonce" on valid form

Problem: Form submission fails nonce verification.

Cause: Action name mismatch or nonce expired.

Fix: Ensure action names match exactly:

// Form generation
$token = $nonce->create('save_settings');
echo '<input type="hidden" name="_nonce" value="' . esc_attr($token) . '">';

// Verification (must match 'save_settings')
$nonce->verify($_POST['_nonce'], 'save_settings');

Nonce valid for wrong plugin

Problem: Plugin A accepts nonce from Plugin B.

Cause: Both plugins use same action name without prefixing.

Fix: Use context-prefixed Nonce class:

// Plugin A: creates "plugina_save" action
$nonceA = new Nonce($contextA);
$tokenA = $nonceA->create('save');

// Plugin B: creates "pluginb_save" action
$nonceB = new Nonce($contextB);
$nonceB->verify($tokenA, 'save'); // Fails (different prefix)

Nonce expires too quickly

Problem: Form valid for < 12 hours.

Cause: WordPress nonces have 24-hour lifetime (12h full + 12h grace).

Fix: This is expected behavior. For longer forms:

// Refresh nonce via AJAX
$hooks->wpAction('wp_ajax_refresh_nonce', function () use ($nonce) {
    wp_send_json_success([
        'nonce' => $nonce->create('save_settings'),
    ]);
});

Gate Issues

Capability check always fails

Problem: $gate->can('manage_options') returns false for admin.

Cause: Called before init when user not loaded.

Fix: Check capabilities after WordPress loads user:

$hooks->wpAction('init', function () use ($gate) {
    if ($gate->can('manage_options')) {
        // Now works
    }
});

Custom capability not recognized

Problem: $gate->can('myplugin_edit') always false.

Cause: Custom capability not registered.

Fix: Add capability to role:

$hooks->wpAction('init', function () use ($context) {
    $role = get_role('administrator');
    $role->add_cap($context->prefix() . 'edit');
});

Sanitizer Issues

HTML stripped unexpectedly

Problem: $sanitizer->text($content) removes allowed HTML.

Cause: Wrong sanitizer method for content type.

Fix: Use correct method:

// Plain text (strips all HTML)
$title = $sanitizer->text($input);

// HTML content (allows safe tags)
$content = $sanitizer->html($input);

// Textarea (preserves newlines)
$notes = $sanitizer->textarea($input);

Email sanitization returns empty

Problem: $sanitizer->email($input) returns empty string.

Cause: Invalid email format.

Fix: Validate before assuming valid:

$email = $sanitizer->email($input);
if (empty($email) && !empty($input)) {
    // Invalid email provided
    $errors[] = __('Invalid email format', $context->textDomain());
}

RateLimiter Issues

Rate limit not enforced

Problem: tooManyAttempts() never returns true.

Cause: Transient not persisting or wrong key.

Fix: Verify unique key per user/action:

// Bad: Same key for all users
$limiter->tooManyAttempts('login');

// Good: Unique per IP
$key = 'login_' . $_SERVER['REMOTE_ADDR'];
$limiter->tooManyAttempts($key);

Rate limit persists after clearing

Problem: User still blocked after transient should expire.

Cause: Object cache caching transients.

Fix: Clear object cache:

wp_cache_delete($context->transientKey($key), 'transient');