Usage Guide

This guide covers all security services provided by the wpzylos-security package with practical examples.

Important: All services use instance-based API resolved from the container. Do not use static calls.


Resolving Services

All services are registered by SecurityServiceProvider as singletons. Resolve them from the application container:

use WPZylos\Framework\Security\Nonce;
use WPZylos\Framework\Security\Gate;
use WPZylos\Framework\Security\Sanitizer;
use WPZylos\Framework\Security\RateLimiter;
use WPZylos\Framework\Security\UploadSecurity;

// By class name
$nonce     = $app->make(Nonce::class);
$gate      = $app->make(Gate::class);
$sanitizer = $app->make(Sanitizer::class);
$limiter   = $app->make(RateLimiter::class);
$upload    = $app->make(UploadSecurity::class);

// Or by string alias
$nonce     = $app->make('nonce');
$gate      = $app->make('gate');
$sanitizer = $app->make('sanitizer');
$limiter   = $app->make('rate-limiter');
$upload    = $app->make('upload-security');

Nonce Management

The Nonce class wraps WordPress nonce functions with automatic plugin-scoped action prefixing.

Creating and Verifying Nonces

$nonce = $app->make(Nonce::class);

// Create a nonce token
$token = $nonce->create('save_settings');

// Verify a nonce
if ($nonce->verify($_POST['_wpnonce'], 'save_settings')) {
    // Nonce is valid — proceed
}

Form Protection

// In your form template
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
    <?php
    $nonce = $app->make(Nonce::class);
    $nonce->field('save_settings'); // echoes hidden input + referer
    ?>
    <input type="hidden" name="action" value="my_plugin_save">
    <button type="submit">Save</button>
</form>
// Non-echoing usage (e.g., in a variable)
$fieldHtml = $nonce->field('save_settings', '_wpnonce', true, false);

URL Nonce

$nonce = $app->make(Nonce::class);

$deleteUrl = $nonce->url(
    admin_url('admin.php?action=delete&id=42'),
    'delete_item'
);
// Result: admin.php?action=delete&id=42&_wpnonce=abc123

Admin & AJAX Referer Checks

$nonce = $app->make(Nonce::class);

// Admin page form processing
$nonce->checkAdminReferer('save_settings');

// AJAX handler
$nonce->checkAjaxReferer('ajax_action', '_wpnonce', true);

Authorization Gate

The Gate class provides a fluent API for checking WordPress user capabilities.

Basic Capability Checks

$gate = $app->make(Gate::class);

// Check a single capability
if ($gate->can('edit_posts')) {
    // Current user can edit posts
}

// Inverse check
if ($gate->cannot('manage_options')) {
    wp_die('Admin access required.');
}

// Check capability for a specific post
if ($gate->can('edit_post', $postId)) {
    // Can edit this specific post
}

Convenience Methods

$gate = $app->make(Gate::class);

if ($gate->isAdmin()) {
    // User has manage_options
}

if ($gate->canManagePlugin()) {
    // Same as isAdmin() by default
}

// Override the required capability
if ($gate->canManagePlugin('edit_others_posts')) {
    // Editors and above
}

if ($gate->canEdit()) {
    // User has edit_posts
}

if ($gate->canPublish()) {
    // User has publish_posts
}

if ($gate->canDelete()) {
    // User has delete_posts
}

Authorization (Abort on Failure)

$gate = $app->make(Gate::class);

// Dies with 403 if user lacks the capability
$gate->authorize('manage_options');

// Custom error message
$gate->authorize(
    'manage_options',
    'Only administrators can access this page.',
    403
);

User State

$gate = $app->make(Gate::class);

if ($gate->isLoggedIn()) {
    $id = $gate->userId();
    // Logged-in user with ID
}

if ($gate->isGuest()) {
    // Not logged in
}

Checking Another User

$gate = $app->make(Gate::class);

if ($gate->userCan($userId, 'edit_posts')) {
    // Specific user can edit posts
}

Multiple Capabilities

$gate = $app->make(Gate::class);

// User has ANY of these capabilities
if ($gate->canAny(['edit_posts', 'upload_files'])) {
    // At least one matches
}

// User has ALL of these capabilities
if ($gate->canAll(['edit_posts', 'publish_posts', 'upload_files'])) {
    // All three are present
}

Input Sanitization

The Sanitizer class provides field-level sanitization using WordPress sanitization functions.

Individual Sanitizers

$sanitizer = $app->make(Sanitizer::class);

$title   = $sanitizer->text($_POST['title']);
$bio     = $sanitizer->textarea($_POST['bio']);
$content = $sanitizer->html($_POST['content']);
$email   = $sanitizer->email($_POST['email']);
$website = $sanitizer->url($_POST['website']);
$count   = $sanitizer->int($_POST['count']);
$postId  = $sanitizer->absint($_POST['post_id']);
$price   = $sanitizer->float($_POST['price']);
$active  = $sanitizer->bool($_POST['active']);
$slug    = $sanitizer->slug($_POST['slug']);
$key     = $sanitizer->key($_POST['option_key']);
$file    = $sanitizer->filename($_POST['filename']);
$class   = $sanitizer->htmlClass($_POST['css_class']);

URL with Custom Protocols

$sanitizer = $app->make(Sanitizer::class);

// Default: http, https
$url = $sanitizer->url($input);

// Allow additional protocols
$url = $sanitizer->url($input, ['http', 'https', 'ftp', 'mailto']);

Dynamic Type-Based Sanitization

$sanitizer = $app->make(Sanitizer::class);

// Sanitize by type name string
$clean = $sanitizer->sanitize($value, 'email');
$clean = $sanitizer->sanitize($value, 'int');
$clean = $sanitizer->sanitize($value, 'html');

Array Sanitization

$sanitizer = $app->make(Sanitizer::class);

// Sanitize all elements with the same type
$cleanTags = $sanitizer->array($_POST['tags'], 'text');
$cleanIds  = $sanitizer->array($_POST['ids'], 'absint');

Bulk Sanitization with sanitizeMany

$sanitizer = $app->make(Sanitizer::class);

$clean = $sanitizer->sanitizeMany($_POST, [
    'title'    => 'text',
    'email'    => 'email',
    'content'  => 'html',
    'website'  => 'url',
    'post_id'  => 'absint',
    'price'    => 'float',
    'active'   => 'bool',
    'slug'     => 'slug',
]);

// $clean only contains keys that exist in both $_POST and the type map
echo $clean['title'];  // sanitized
echo $clean['email'];  // sanitized

Rate Limiting

The RateLimiter class provides request throttling using WordPress transients. Compatible with persistent object cache.

Basic Usage

$limiter = $app->make(RateLimiter::class);

// Generate a key scoped to the current user (falls back to IP for guests)
$key = $limiter->forUser('login');

// Check if rate limited
if ($limiter->tooManyAttempts($key)) {
    $wait = $limiter->availableIn($key);
    wp_send_json_error([
        'message' => "Too many attempts. Try again in {$wait} seconds.",
    ], 429);
}

// Record a hit
$limiter->hit($key);

Login Throttling Example

$limiter = $app->make(RateLimiter::class);

$key = $limiter->forIp('login');

if ($limiter->tooManyAttempts($key)) {
    $seconds = $limiter->availableIn($key);
    wp_die("Too many login attempts. Please wait {$seconds} seconds.");
}

$limiter->hit($key);

// Process login...

// On successful login, clear the limiter
$limiter->clear($key);

Using attempt() with Callbacks

$limiter = $app->make(RateLimiter::class);

$key = $limiter->forUser('api_call');

$result = $limiter->attempt(
    $key,
    // Allowed callback
    function () {
        return process_api_request();
    },
    // Limited callback (optional)
    function (int $waitSeconds) {
        wp_send_json_error([
            'message' => "Rate limited. Retry in {$waitSeconds}s.",
            'retry_after' => $waitSeconds,
        ], 429);
    }
);

Checking Remaining Attempts

$limiter = $app->make(RateLimiter::class);

$key = $limiter->forUser('api_call');
$remaining = $limiter->remaining($key);

// Include in response headers
header("X-RateLimit-Remaining: {$remaining}");

IP-Based Rate Limiting

$limiter = $app->make(RateLimiter::class);

// IP-based key (supports Cloudflare, X-Forwarded-For, X-Real-IP)
$key = $limiter->forIp('contact_form');

$limiter->hit($key);

if ($limiter->tooManyAttempts($key)) {
    // Blocked
}

File Upload Security

The UploadSecurity class wraps WordPress uploads with nonce verification, capability checks, MIME validation, and size limits.

Single File Upload

$upload = $app->make(UploadSecurity::class);

$result = $upload->handle($_FILES['avatar'], 'upload_avatar');

if (is_wp_error($result)) {
    echo $result->get_error_message();
} else {
    $filePath = $result['file']; // Server path
    $fileUrl  = $result['url'];  // Public URL
    $fileType = $result['type']; // MIME type
}

Multiple File Uploads

$upload = $app->make(UploadSecurity::class);

$results = $upload->handleMultiple($_FILES['documents'], 'upload_documents');

foreach ($results as $result) {
    if (is_wp_error($result)) {
        echo 'Error: ' . $result->get_error_message();
    } else {
        echo 'Uploaded: ' . $result['url'];
    }
}

Custom MIME Types

$upload = $app->make(UploadSecurity::class);

$result = $upload
    ->allowMimes([
        'jpg|jpeg' => 'image/jpeg',
        'png'      => 'image/png',
        'webp'     => 'image/webp',
    ])
    ->handle($_FILES['photo'], 'upload_photo');

Custom Max File Size

$upload = $app->make(UploadSecurity::class);

$result = $upload
    ->maxSize(2 * 1024 * 1024) // 2 MB
    ->handle($_FILES['thumbnail'], 'upload_thumbnail');

Chaining Configuration

$upload = $app->make(UploadSecurity::class);

$result = $upload
    ->allowMimes(['pdf' => 'application/pdf'])
    ->maxSize(10 * 1024 * 1024) // 10 MB
    ->handle($_FILES['document'], 'upload_document', 'upload_files');

Custom Capability Requirement

$upload = $app->make(UploadSecurity::class);

// Require manage_options instead of the default upload_files
$result = $upload->handle($_FILES['config'], 'upload_config', 'manage_options');

Form Example

// Template
<form method="post" enctype="multipart/form-data">
    <?php $app->make(Nonce::class)->field('upload_avatar'); ?>
    <input type="file" name="avatar">
    <button type="submit">Upload</button>
</form>
// Handler
$upload = $app->make(UploadSecurity::class);
$result = $upload->handle($_FILES['avatar'], 'upload_avatar');

if (is_wp_error($result)) {
    // Handle error
} else {
    update_user_meta(get_current_user_id(), 'avatar_url', $result['url']);
}

Middleware

AuthMiddleware

Checks user capabilities before allowing the request to proceed. Calls wp_die() with 403 on failure.

use WPZylos\Framework\Security\Middleware\AuthMiddleware;

$gate = $app->make(Gate::class);

// Require a single capability
$middleware = new AuthMiddleware($gate, 'manage_options');
$middleware->handle($request, $next);

// Require ANY of multiple capabilities
$middleware = AuthMiddleware::any($gate, ['edit_posts', 'upload_files']);
$middleware->handle($request, $next);

// Require ALL of multiple capabilities
$middleware = AuthMiddleware::all($gate, ['edit_posts', 'publish_posts']);
$middleware->handle($request, $next);

NonceMiddleware

Verifies CSRF nonce before allowing the request to proceed. Extracts the nonce from POST, GET, or a request object's input() method.

use WPZylos\Framework\Security\Middleware\NonceMiddleware;

$nonce = $app->make(Nonce::class);

// Constructor
$middleware = new NonceMiddleware($nonce, 'save_settings');
$middleware->handle($request, $next);

// Factory method
$middleware = NonceMiddleware::for($nonce, 'save_settings');
$middleware->handle($request, $next);

// Custom field name
$middleware = NonceMiddleware::for($nonce, 'save_settings', 'my_nonce_field');

Escaping Helpers

Global helper functions for escaping output in templates. These are available everywhere once the package is loaded.

HTML Escaping

<h1><?php echo wpzylos_e($title); ?></h1>
<p><?php echo wpzylos_e($description); ?></p>

Attribute Escaping

<input type="text" value="<?php echo wpzylos_ea($value); ?>">
<div class="<?php echo wpzylos_ea($cssClass); ?>">

URL Escaping

<a href="<?php echo wpzylos_eu($link); ?>">Click here</a>
<img src="<?php echo wpzylos_eu($imageUrl); ?>" alt="">

JavaScript Escaping

<script>
    var message = '<?php echo wpzylos_ej($message); ?>';
</script>

HTML Filtering

// Allow post-level HTML tags (default)
<?php echo wpzylos_kses($userContent); ?>

// Allow only data-level HTML tags
<?php echo wpzylos_kses($widgetContent, 'data'); ?>

// Strip all HTML
<?php echo wpzylos_kses($rawInput, 'strip'); ?>

JSON Embedding

<script>
    var config = <?php echo wpzylos_e_json($configArray); ?>;
</script>

The JSON is encoded with JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP flags by default, making it safe for embedding in HTML <script> tags. Throws \JsonException on encoding failure.