wordpress-plugin-core
Build secure WordPress plugins with core patterns for hooks, database interactions, Settings API, custom post types, REST API, and AJAX. Covers three architecture patterns (Simple, OOP, PSR-4) and the Security Trinity. Use when creating plugins, implementing nonces/sanitization/escaping, working with $wpdb prepared statements, or troubleshooting SQL injection, XSS, CSRF vulnerabilities, or plugin activation errors.
What this skill does
# WordPress Plugin Development (Core)
**Status**: Production Ready
**Last Updated**: 2025-11-06
**Dependencies**: None (WordPress 5.9+, PHP 7.4+)
**Latest Versions**: WordPress 6.7+, PHP 8.0+ recommended
---
## Quick Start (10 Minutes)
### 1. Choose Your Plugin Structure
WordPress plugins can use three architecture patterns:
- **Simple** (functions only) - For small plugins with <5 functions
- **OOP** (Object-Oriented) - For medium plugins with related functionality
- **PSR-4** (Namespaced + Composer autoload) - For large/modern plugins
**Why this matters:**
- Simple plugins are easiest to start but don't scale well
- OOP provides organization without modern PHP features
- PSR-4 is the modern standard (2025) and most maintainable
### 2. Create Plugin Header
Every plugin MUST have a header comment in the main file:
```php
<?php
/**
* Plugin Name: My Awesome Plugin
* Plugin URI: https://example.com/my-plugin/
* Description: Brief description of what this plugin does.
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Author: Your Name
* Author URI: https://yoursite.com/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-plugin
* Domain Path: /languages
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
```
**CRITICAL:**
- Plugin Name is the ONLY required field
- Text Domain must match plugin slug exactly (for translations)
- Always add ABSPATH check to prevent direct file access
### 3. Implement The Security Foundation
Before writing ANY functionality, implement these 5 security essentials:
```php
// 1. Unique Prefix (4-5 chars minimum)
define( 'MYPL_VERSION', '1.0.0' );
function mypl_init() {
// Your code
}
add_action( 'init', 'mypl_init' );
// 2. ABSPATH Check (every PHP file)
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// 3. Nonces for Forms
<input type="hidden" name="mypl_nonce" value="<?php echo wp_create_nonce( 'mypl_action' ); ?>" />
// 4. Sanitize Input, Escape Output
$clean = sanitize_text_field( $_POST['input'] );
echo esc_html( $output );
// 5. Prepared Statements for Database
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}table WHERE id = %d",
$id
)
);
```
---
## The 5-Step Security Foundation
WordPress plugin security has THREE components that must ALL be present:
### Step 1: Use Unique Prefix for Everything
**Why**: Prevents naming conflicts with other plugins and WordPress core.
**Rules**:
- 4-5 characters minimum
- Apply to: functions, classes, constants, options, transients, meta keys, global variables
- Avoid: `wp_`, `__`, `_`, "WordPress"
```php
// GOOD
function mypl_function_name() {}
class MyPL_Class_Name {}
define( 'MYPL_CONSTANT', 'value' );
add_option( 'mypl_option', 'value' );
set_transient( 'mypl_cache', $data, HOUR_IN_SECONDS );
// BAD
function function_name() {} // No prefix, will conflict
class Settings {} // Too generic
```
### Step 2: Check Capabilities, Not Just Admin Status
**ERROR**: Using `is_admin()` for permission checks
```php
// WRONG - Anyone can access admin area URLs
if ( is_admin() ) {
// Delete user data - SECURITY HOLE
}
// CORRECT - Check user capability
if ( current_user_can( 'manage_options' ) ) {
// Delete user data - Now secure
}
```
**Common Capabilities**:
- `manage_options` - Administrator
- `edit_posts` - Editor/Author
- `publish_posts` - Author
- `edit_pages` - Editor
- `read` - Subscriber
### Step 3: The Security Trinity
**Input → Processing → Output** each require different functions:
```php
// SANITIZATION (Input) - Clean user data
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$url = esc_url_raw( $_POST['url'] );
$html = wp_kses_post( $_POST['content'] ); // Allow safe HTML
$key = sanitize_key( $_POST['option'] );
$ids = array_map( 'absint', $_POST['ids'] ); // Array of integers
// VALIDATION (Logic) - Verify it meets requirements
if ( ! is_email( $email ) ) {
wp_die( 'Invalid email' );
}
// ESCAPING (Output) - Make safe for display
echo esc_html( $name );
echo '<a href="' . esc_url( $url ) . '">';
echo '<div class="' . esc_attr( $class ) . '">';
echo '<textarea>' . esc_textarea( $content ) . '</textarea>';
```
**Critical Rule**: Sanitize on INPUT, escape on OUTPUT. Never trust user data.
### Step 4: Nonces (CSRF Protection)
**What**: One-time tokens that prove requests came from your site.
**Form Pattern**:
```php
// Generate nonce in form
<form method="post">
<?php wp_nonce_field( 'mypl_action', 'mypl_nonce' ); ?>
<input type="text" name="data" />
<button type="submit">Submit</button>
</form>
// Verify nonce in handler
if ( ! isset( $_POST['mypl_nonce'] ) || ! wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' ) ) {
wp_die( 'Security check failed' );
}
// Now safe to proceed
$data = sanitize_text_field( $_POST['data'] );
```
**AJAX Pattern**:
```javascript
// JavaScript
jQuery.ajax({
url: ajaxurl,
data: {
action: 'mypl_ajax_action',
nonce: mypl_ajax_object.nonce,
data: formData
}
});
```
```php
// PHP Handler
function mypl_ajax_handler() {
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
// Safe to proceed
wp_send_json_success( array( 'message' => 'Success' ) );
}
add_action( 'wp_ajax_mypl_ajax_action', 'mypl_ajax_handler' );
// Localize script with nonce
wp_localize_script( 'mypl-script', 'mypl_ajax_object', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ),
) );
```
### Step 5: Prepared Statements for Database
**CRITICAL**: Always use `$wpdb->prepare()` for queries with user input.
```php
global $wpdb;
// WRONG - SQL Injection vulnerability
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" );
// CORRECT - Prepared statement
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}table WHERE id = %d",
$_GET['id']
)
);
```
**Placeholders**:
- `%s` - String
- `%d` - Integer
- `%f` - Float
**LIKE Queries** (Special Case):
```php
$search = '%' . $wpdb->esc_like( $term ) . '%';
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}posts WHERE post_title LIKE %s",
$search
)
);
```
---
## Critical Rules
### Always Do
✅ **Use unique prefix** (4-5 chars) for all global code (functions, classes, options, transients)
✅ **Add ABSPATH check** to every PHP file: `if ( ! defined( 'ABSPATH' ) ) exit;`
✅ **Check capabilities** (`current_user_can()`) not just `is_admin()`
✅ **Verify nonces** for all forms and AJAX requests
✅ **Use $wpdb->prepare()** for all database queries with user input
✅ **Sanitize input** with `sanitize_*()` functions before saving
✅ **Escape output** with `esc_*()` functions before displaying
✅ **Flush rewrite rules** on activation when registering custom post types
✅ **Use uninstall.php** for permanent cleanup (not deactivation hook)
✅ **Follow WordPress Coding Standards** (tabs for indentation, Yoda conditions)
### Never Do
❌ **Never use extract()** - Creates security vulnerabilities
❌ **Never trust $_POST/$_GET** without sanitization
❌ **Never concatenate user input into SQL** - Always use prepare()
❌ **Never use `is_admin()` alone** for permission checks
❌ **Never output unsanitized data** - Always escape
❌ **Never use generic function/class names** - Always prefix
❌ **Never use short PHP tags** `<?` or `<?=` - Use `<?php` only
❌ **Never delete user data on deactivation** - Only on uninstall
❌ **Never register uninstall hook repeatedly** - Only once on activation
❌ **Never use `register_uninstall_hook()` in main flow** - Use uninstall.php instead
---
## Known Issues Prevention
This skill prevents **20** documented issues:
### Issue #1: SQL InjectionRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.