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.