
Wordpress Plugin Core
- 55 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
Helps with ai & agent building tasks during AI-assisted development.
About
wordpress-plugin-core is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- wordpress-plugin-core
- AI & Agent Building
- AI-coding skill
Wordpress Plugin Core by the numbers
- 55 all-time installs (skills.sh)
- Ranked #6,703 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill wordpress-plugin-coreAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 55 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
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
/**
* 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:
// 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"
// 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 genericStep 2: Check Capabilities, Not Just Admin Status
ERROR: Using is_admin() for permission checks
// 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- Administratoredit_posts- Editor/Authorpublish_posts- Authoredit_pages- Editorread- Subscriber
Step 3: The Security Trinity
Input → Processing → Output each require different functions:
// 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:
// 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
jQuery.ajax({
url: ajaxurl,
data: {
action: 'mypl_ajax_action',
nonce: mypl_ajax_object.nonce,
data: formData
}
});// 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.
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):
$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 Injection
Error: Database compromised via unescaped user input Source: https://patchstack.com/articles/sql-injection/ (15% of all vulnerabilities) Why It Happens: Direct concatenation of user input into SQL queries Prevention: Always use $wpdb->prepare() with placeholders
// VULNERABLE
$wpdb->query( "DELETE FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" );
// SECURE
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );Issue #2: XSS (Cross-Site Scripting)
Error: Malicious JavaScript executed in user browsers Source: https://patchstack.com (35% of all vulnerabilities) Why It Happens: Outputting unsanitized user data to HTML Prevention: Always escape output with context-appropriate function
// VULNERABLE
echo $_POST['name'];
echo '<div class="' . $_POST['class'] . '">';
// SECURE
echo esc_html( $_POST['name'] );
echo '<div class="' . esc_attr( $_POST['class'] ) . '">';Issue #3: CSRF (Cross-Site Request Forgery)
Error: Unauthorized actions performed on behalf of users Source: https://blog.nintechnet.com/25-wordpress-plugins-vulnerable-to-csrf-attacks/ Why It Happens: No verification that requests originated from your site Prevention: Use nonces with wp_nonce_field() and wp_verify_nonce()
// VULNERABLE
if ( $_POST['action'] == 'delete' ) {
delete_user( $_POST['user_id'] );
}
// SECURE
if ( ! wp_verify_nonce( $_POST['nonce'], 'mypl_delete_user' ) ) {
wp_die( 'Security check failed' );
}
delete_user( absint( $_POST['user_id'] ) );Issue #4: Missing Capability Checks
Error: Regular users can access admin functions Source: WordPress Security Review Guidelines Why It Happens: Using is_admin() instead of current_user_can() Prevention: Always check capabilities, not just admin context
// VULNERABLE
if ( is_admin() ) {
// Any logged-in user can trigger this
}
// SECURE
if ( current_user_can( 'manage_options' ) ) {
// Only administrators can trigger this
}Issue #5: Direct File Access
Error: PHP files executed outside WordPress context Source: WordPress Plugin Handbook Why It Happens: No ABSPATH check at top of file Prevention: Add ABSPATH check to every PHP file
// Add to top of EVERY PHP file
if ( ! defined( 'ABSPATH' ) ) {
exit;
}Issue #6: Prefix Collision
Error: Functions/classes conflict with other plugins Source: WordPress Coding Standards Why It Happens: Generic names without unique prefix Prevention: Use 4-5 character prefix on ALL global code
// CAUSES CONFLICTS
function init() {}
class Settings {}
add_option( 'api_key', $value );
// SAFE
function mypl_init() {}
class MyPL_Settings {}
add_option( 'mypl_api_key', $value );Issue #7: Rewrite Rules Not Flushed
Error: Custom post types return 404 errors Source: WordPress Plugin Handbook Why It Happens: Forgot to flush rewrite rules after registering CPT Prevention: Flush on activation, clear on deactivation
function mypl_activate() {
mypl_register_cpt();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );Issue #8: Transients Not Cleaned
Error: Database accumulates expired transients Source: WordPress Transients API Documentation Why It Happens: No cleanup on uninstall Prevention: Delete transients in uninstall.php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mypl_%'" );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mypl_%'" );Issue #9: Scripts Loaded Everywhere
Error: Performance degraded by unnecessary asset loading Source: WordPress Performance Best Practices Why It Happens: Enqueuing scripts/styles without conditional checks Prevention: Only load assets where needed
// BAD - Loads on every page
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script( 'mypl-script', $url );
} );
// GOOD - Only loads on specific page
add_action( 'wp_enqueue_scripts', function() {
if ( is_page( 'my-page' ) ) {
wp_enqueue_script( 'mypl-script', $url, array( 'jquery' ), '1.0', true );
}
} );Issue #10: Missing Sanitization on Save
Error: Malicious data stored in database Source: WordPress Data Validation Why It Happens: Saving $_POST data without sanitization Prevention: Always sanitize before saving
// VULNERABLE
update_option( 'mypl_setting', $_POST['value'] );
// SECURE
update_option( 'mypl_setting', sanitize_text_field( $_POST['value'] ) );Issue #11: Incorrect LIKE Queries
Error: SQL syntax errors or injection vulnerabilities Source: WordPress $wpdb Documentation Why It Happens: LIKE wildcards not escaped properly Prevention: Use $wpdb->esc_like()
// WRONG
$search = '%' . $term . '%';
// CORRECT
$search = '%' . $wpdb->esc_like( $term ) . '%';
$results = $wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );Issue #12: Using extract()
Error: Variable collision and security vulnerabilities Source: WordPress Coding Standards Why It Happens: extract() creates variables from array keys Prevention: Never use extract(), access array elements directly
// DANGEROUS
extract( $_POST );
// Now $any_array_key becomes a variable
// SAFE
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';Issue #13: Missing Permission Callback in REST API
Error: Endpoints accessible to everyone Source: WordPress REST API Handbook Why It Happens: No permission_callback specified Prevention: Always add permission_callback
// VULNERABLE
register_rest_route( 'myplugin/v1', '/data', array(
'callback' => 'my_callback',
) );
// SECURE
register_rest_route( 'myplugin/v1', '/data', array(
'callback' => 'my_callback',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );Issue #14: Uninstall Hook Registered Repeatedly
Error: Option written on every page load Source: WordPress Plugin Handbook Why It Happens: register_uninstall_hook() called in main flow Prevention: Use uninstall.php file instead
// BAD - Runs on every page load
register_uninstall_hook( __FILE__, 'mypl_uninstall' );
// GOOD - Use uninstall.php file (preferred method)
// Create uninstall.php in plugin rootIssue #15: Data Deleted on Deactivation
Error: Users lose data when temporarily disabling plugin Source: WordPress Plugin Development Best Practices Why It Happens: Confusion about deactivation vs uninstall Prevention: Only delete data in uninstall.php, never on deactivation
// WRONG - Deletes user data on deactivation
register_deactivation_hook( __FILE__, function() {
delete_option( 'mypl_user_settings' );
} );
// CORRECT - Only clear temporary data on deactivation
register_deactivation_hook( __FILE__, function() {
delete_transient( 'mypl_cache' );
} );
// CORRECT - Delete all data in uninstall.phpIssue #16: Using Deprecated Functions
Error: Plugin breaks on WordPress updates Source: WordPress Deprecated Functions List Why It Happens: Using functions removed in newer WordPress versions Prevention: Enable WP_DEBUG during development
// In wp-config.php (development only)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );Issue #17: Text Domain Mismatch
Error: Translations don't load Source: WordPress Internationalization Why It Happens: Text domain doesn't match plugin slug Prevention: Use exact plugin slug everywhere
// Plugin header
// Text Domain: my-plugin
// In code - MUST MATCH EXACTLY
__( 'Text', 'my-plugin' );
_e( 'Text', 'my-plugin' );Issue #18: Missing Plugin Dependencies
Error: Fatal error when required plugin is inactive Source: WordPress Plugin Dependencies Why It Happens: No check for required plugins Prevention: Check for dependencies on plugins_loaded
add_action( 'plugins_loaded', function() {
if ( ! class_exists( 'WooCommerce' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>My Plugin requires WooCommerce.</p></div>';
} );
return;
}
// Initialize plugin
} );Issue #19: Autosave Triggering Meta Save
Error: Meta saved multiple times, performance issues Source: WordPress Post Meta Why It Happens: No autosave check in save_post hook Prevention: Check for DOING_AUTOSAVE constant
add_action( 'save_post', function( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Safe to save meta
} );Issue #20: admin-ajax.php Performance
Error: Slow AJAX responses Source: https://deliciousbrains.com/comparing-wordpress-rest-api-performance-admin-ajax-php/ Why It Happens: admin-ajax.php loads entire WordPress core Prevention: Use REST API for new projects (10x faster)
// OLD: admin-ajax.php (still works but slower)
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
// NEW: REST API (10x faster, recommended)
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/endpoint', array(
'methods' => 'POST',
'callback' => 'mypl_rest_handler',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );
} );---
Plugin Architecture Patterns
Pattern 1: Simple Plugin (Functions Only)
When to use: Small plugins with <5 functions, no complex state
<?php
/**
* Plugin Name: Simple Plugin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function mypl_init() {
// Your code here
}
add_action( 'init', 'mypl_init' );
function mypl_admin_menu() {
add_options_page(
'My Plugin',
'My Plugin',
'manage_options',
'my-plugin',
'mypl_settings_page'
);
}
add_action( 'admin_menu', 'mypl_admin_menu' );
function mypl_settings_page() {
?>
<div class="wrap">
<h1>My Plugin Settings</h1>
</div>
<?php
}Pattern 2: OOP Plugin
When to use: Medium plugins with related functionality, need organization
<?php
/**
* Plugin Name: OOP Plugin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class MyPL_Plugin {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->define_constants();
$this->init_hooks();
}
private function define_constants() {
define( 'MYPL_VERSION', '1.0.0' );
define( 'MYPL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MYPL_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
}
private function init_hooks() {
add_action( 'init', array( $this, 'init' ) );
add_action( 'admin_menu', array( $this, 'admin_menu' ) );
}
public function init() {
// Initialization code
}
public function admin_menu() {
add_options_page(
'My Plugin',
'My Plugin',
'manage_options',
'my-plugin',
array( $this, 'settings_page' )
);
}
public function settings_page() {
?>
<div class="wrap">
<h1>My Plugin Settings</h1>
</div>
<?php
}
}
// Initialize plugin
function mypl() {
return MyPL_Plugin::get_instance();
}
mypl();Pattern 3: PSR-4 Plugin (Modern, Recommended)
When to use: Large/modern plugins, team development, 2025+ best practice
Directory Structure:
my-plugin/
├── my-plugin.php # Main file
├── composer.json # Autoloading config
├── src/ # PSR-4 autoloaded classes
│ ├── Admin.php
│ ├── Frontend.php
│ └── Settings.php
├── languages/
└── uninstall.phpcomposer.json:
{
"name": "my-vendor/my-plugin",
"autoload": {
"psr-4": {
"MyPlugin\\": "src/"
}
},
"require": {
"php": ">=7.4"
}
}my-plugin.php:
<?php
/**
* Plugin Name: PSR-4 Plugin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
use MyPlugin\Admin;
use MyPlugin\Frontend;
class MyPlugin {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init();
}
private function init() {
new Admin();
new Frontend();
}
}
MyPlugin::get_instance();src/Admin.php:
<?php
namespace MyPlugin;
class Admin {
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu' ) );
}
public function add_menu() {
add_options_page(
'My Plugin',
'My Plugin',
'manage_options',
'my-plugin',
array( $this, 'settings_page' )
);
}
public function settings_page() {
?>
<div class="wrap">
<h1>My Plugin Settings</h1>
</div>
<?php
}
}---
Common Patterns
Pattern 1: Custom Post Types
function mypl_register_cpt() {
register_post_type( 'book', array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
'add_new_item' => 'Add New Book',
),
'public' => true,
'has_archive' => true,
'show_in_rest' => true, // Gutenberg support
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'rewrite' => array( 'slug' => 'books' ),
'menu_icon' => 'dashicons-book',
) );
}
add_action( 'init', 'mypl_register_cpt' );
// CRITICAL: Flush rewrite rules on activation
function mypl_activate() {
mypl_register_cpt();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );Pattern 2: Custom Taxonomies
function mypl_register_taxonomy() {
register_taxonomy( 'genre', 'book', array(
'labels' => array(
'name' => 'Genres',
'singular_name' => 'Genre',
),
'hierarchical' => true, // Like categories
'show_in_rest' => true,
'rewrite' => array( 'slug' => 'genre' ),
) );
}
add_action( 'init', 'mypl_register_taxonomy' );Pattern 3: Meta Boxes
function mypl_add_meta_box() {
add_meta_box(
'book_details',
'Book Details',
'mypl_meta_box_html',
'book',
'normal',
'high'
);
}
add_action( 'add_meta_boxes', 'mypl_add_meta_box' );
function mypl_meta_box_html( $post ) {
$isbn = get_post_meta( $post->ID, '_book_isbn', true );
wp_nonce_field( 'mypl_save_meta', 'mypl_meta_nonce' );
?>
<label for="book_isbn">ISBN:</label>
<input type="text" id="book_isbn" name="book_isbn" value="<?php echo esc_attr( $isbn ); ?>" />
<?php
}
function mypl_save_meta( $post_id ) {
// Security checks
if ( ! isset( $_POST['mypl_meta_nonce'] )
|| ! wp_verify_nonce( $_POST['mypl_meta_nonce'], 'mypl_save_meta' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Save data
if ( isset( $_POST['book_isbn'] ) ) {
update_post_meta(
$post_id,
'_book_isbn',
sanitize_text_field( $_POST['book_isbn'] )
);
}
}
add_action( 'save_post_book', 'mypl_save_meta' );Pattern 4: Settings API
function mypl_add_menu() {
add_options_page(
'My Plugin Settings',
'My Plugin',
'manage_options',
'my-plugin',
'mypl_settings_page'
);
}
add_action( 'admin_menu', 'mypl_add_menu' );
function mypl_register_settings() {
register_setting( 'mypl_options', 'mypl_api_key', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
) );
add_settings_section(
'mypl_section',
'API Settings',
'mypl_section_callback',
'my-plugin'
);
add_settings_field(
'mypl_api_key',
'API Key',
'mypl_field_callback',
'my-plugin',
'mypl_section'
);
}
add_action( 'admin_init', 'mypl_register_settings' );
function mypl_section_callback() {
echo '<p>Configure your API settings.</p>';
}
function mypl_field_callback() {
$value = get_option( 'mypl_api_key' );
?>
<input type="text" name="mypl_api_key" value="<?php echo esc_attr( $value ); ?>" />
<?php
}
function mypl_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields( 'mypl_options' );
do_settings_sections( 'my-plugin' );
submit_button();
?>
</form>
</div>
<?php
}Pattern 5: REST API Endpoints
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'mypl_rest_callback',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
) );
} );
function mypl_rest_callback( $request ) {
$id = $request->get_param( 'id' );
// Process...
return new WP_REST_Response( array(
'success' => true,
'data' => $data,
), 200 );
}Pattern 6: AJAX Handlers (Legacy)
// Enqueue script with localized data
function mypl_enqueue_ajax_script() {
wp_enqueue_script( 'mypl-ajax', plugins_url( 'js/ajax.js', __FILE__ ), array( 'jquery' ), '1.0', true );
wp_localize_script( 'mypl-ajax', 'mypl_ajax_object', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ),
) );
}
add_action( 'wp_enqueue_scripts', 'mypl_enqueue_ajax_script' );
// AJAX handler (logged-in users)
function mypl_ajax_handler() {
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
$data = sanitize_text_field( $_POST['data'] );
// Process...
wp_send_json_success( array( 'message' => 'Success' ) );
}
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
// AJAX handler (logged-out users)
add_action( 'wp_ajax_nopriv_mypl_action', 'mypl_ajax_handler' );JavaScript (js/ajax.js):
jQuery(document).ready(function($) {
$('#my-button').on('click', function() {
$.ajax({
url: mypl_ajax_object.ajaxurl,
type: 'POST',
data: {
action: 'mypl_action',
nonce: mypl_ajax_object.nonce,
data: 'value'
},
success: function(response) {
console.log(response.data.message);
}
});
});
});Pattern 7: Custom Database Tables
function mypl_create_tables() {
global $wpdb;
$table_name = $wpdb->prefix . 'mypl_data';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
data text NOT NULL,
created datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id),
KEY user_id (user_id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
add_option( 'mypl_db_version', '1.0' );
}
// Create tables on activation
register_activation_hook( __FILE__, 'mypl_create_tables' );Pattern 8: Transients for Caching
function mypl_get_expensive_data() {
// Try to get cached data
$data = get_transient( 'mypl_expensive_data' );
if ( false === $data ) {
// Not cached - regenerate
$data = perform_expensive_operation();
// Cache for 12 hours
set_transient( 'mypl_expensive_data', $data, 12 * HOUR_IN_SECONDS );
}
return $data;
}
// Clear cache when data changes
function mypl_clear_cache() {
delete_transient( 'mypl_expensive_data' );
}
add_action( 'save_post', 'mypl_clear_cache' );---
Using Bundled Resources
Templates (templates/)
Use these production-ready templates to scaffold plugins quickly:
templates/plugin-simple/- Simple plugin with functionstemplates/plugin-oop/- Object-oriented plugin structuretemplates/plugin-psr4/- Modern PSR-4 plugin with Composertemplates/examples/meta-box.php- Meta box implementationtemplates/examples/settings-page.php- Settings API pagetemplates/examples/custom-post-type.php- CPT registrationtemplates/examples/rest-endpoint.php- REST API endpointtemplates/examples/ajax-handler.php- AJAX implementation
When Claude should use these: When creating new plugins or implementing specific functionality patterns.
Scripts (scripts/)
scripts/scaffold-plugin.sh- Interactive plugin scaffoldingscripts/check-security.sh- Security audit for common issuesscripts/validate-headers.sh- Verify plugin headers
Example Usage:
# Scaffold new plugin
./scripts/scaffold-plugin.sh my-plugin simple
# Check for security issues
./scripts/check-security.sh my-plugin.php
# Validate plugin headers
./scripts/validate-headers.sh my-plugin.phpReferences (references/)
Detailed documentation that Claude can load when needed:
references/security-checklist.md- Complete security audit checklistreferences/hooks-reference.md- Common WordPress hooks and filtersreferences/sanitization-guide.md- All sanitization/escaping functionsreferences/wpdb-patterns.md- Database query patternsreferences/common-errors.md- Extended error prevention guide
When Claude should load these: When dealing with security issues, choosing the right hook, sanitizing specific data types, writing database queries, or debugging common errors.
---
Advanced Topics
Internationalization (i18n)
// Load text domain
function mypl_load_textdomain() {
load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
add_action( 'plugins_loaded', 'mypl_load_textdomain' );
// Translatable strings
__( 'Text', 'my-plugin' ); // Returns translated string
_e( 'Text', 'my-plugin' ); // Echoes translated string
_n( 'One item', '%d items', $count, 'my-plugin' ); // Plural forms
esc_html__( 'Text', 'my-plugin' ); // Translate and escape
esc_html_e( 'Text', 'my-plugin' ); // Translate, escape, and echoWP-CLI Commands
if ( defined( 'WP_CLI' ) && WP_CLI ) {
class MyPL_CLI_Command {
/**
* Process data
*
* ## EXAMPLES
*
* wp mypl process --limit=100
*
* @param array $args
* @param array $assoc_args
*/
public function process( $args, $assoc_args ) {
$limit = isset( $assoc_args['limit'] ) ? absint( $assoc_args['limit'] ) : 10;
WP_CLI::line( "Processing $limit items..." );
// Process...
WP_CLI::success( 'Processing complete!' );
}
}
WP_CLI::add_command( 'mypl', 'MyPL_CLI_Command' );
}Scheduled Events (Cron)
// Schedule event on activation
function mypl_activate() {
if ( ! wp_next_scheduled( 'mypl_daily_task' ) ) {
wp_schedule_event( time(), 'daily', 'mypl_daily_task' );
}
}
register_activation_hook( __FILE__, 'mypl_activate' );
// Clear event on deactivation
function mypl_deactivate() {
wp_clear_scheduled_hook( 'mypl_daily_task' );
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
// Hook to scheduled event
function mypl_do_daily_task() {
// Perform task
}
add_action( 'mypl_daily_task', 'mypl_do_daily_task' );Plugin Dependencies Check
add_action( 'admin_init', function() {
// Check for WooCommerce
if ( ! class_exists( 'WooCommerce' ) ) {
deactivate_plugins( plugin_basename( __FILE__ ) );
add_action( 'admin_notices', function() {
echo '<div class="error"><p><strong>My Plugin</strong> requires WooCommerce to be installed and active.</p></div>';
} );
if ( isset( $_GET['activate'] ) ) {
unset( $_GET['activate'] );
}
}
} );---
Distribution & Auto-Updates
Enabling GitHub Auto-Updates
Plugins hosted outside WordPress.org can still provide automatic updates using Plugin Update Checker by YahnisElsts. This is the recommended solution for most use cases.
Quick Start:
// 1. Install library (git submodule or Composer)
git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
// 2. Add to main plugin file
require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin-slug'
);
// Use GitHub Releases (recommended)
$updateChecker->getVcsApi()->enableReleaseAssets();
// For private repos, use token from wp-config.php
if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) {
$updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN );
}Deployment:
# 1. Update version in plugin header
# 2. Commit and tag
git add my-plugin.php
git commit -m "Bump version to 1.0.1"
git tag 1.0.1
git push origin main
git push origin 1.0.1
# 3. Create GitHub Release (optional but recommended)
# - Upload pre-built ZIP file (exclude .git, tests, etc.)
# - Add release notes for usersKey Features:
✅ Works with GitHub, GitLab, BitBucket, or custom servers ✅ Supports public and private repositories ✅ Uses GitHub Releases or tags for versioning ✅ Secure HTTPS-based updates ✅ Optional license key integration ✅ Professional release notes and changelogs ✅ ~100KB library footprint
Alternative Solutions:
1. Git Updater (user-installable plugin, no coding required) 2. Custom Update Server (full control, requires hosting) 3. Freemius (commercial, includes licensing and payments)
Comprehensive Resources:
- Complete Guide: See
references/github-auto-updates.md(21 pages, all approaches) - Implementation Examples: See
examples/github-updater.php(10 examples) - Security Best Practices: Checksums, signing, token storage, rate limiting
- Template Integration: All 3 plugin templates include setup instructions
Security Considerations:
- ✅ Always use HTTPS for repository URLs
- ✅ Never hardcode authentication tokens (use wp-config.php)
- ✅ Implement license validation before offering updates
- ✅ Optional: Add checksums for file verification
- ✅ Rate limit update checks to avoid API throttling
- ✅ Clear cached update data after installation
When to Use Each Approach:
| Use Case | Recommended Solution |
|---|---|
| Open source, public repo | Plugin Update Checker |
| Private plugin, client work | Plugin Update Checker + private repo |
| Commercial plugin | Freemius or Custom Server |
| Multi-platform Git hosting | Git Updater |
| Custom licensing needs | Custom Update Server |
ZIP Structure Requirement:
plugin.zip
└── my-plugin/ ← Plugin folder MUST be inside ZIP
├── my-plugin.php
├── readme.txt
└── ...Incorrect structure will cause WordPress to create a random folder name and break the plugin!
---
Dependencies
Required:
- WordPress 5.9+ (recommend 6.7+)
- PHP 7.4+ (recommend 8.0+)
Optional:
- Composer 2.0+ - For PSR-4 autoloading
- WP-CLI 2.0+ - For command-line plugin management
- Query Monitor - For debugging and performance analysis
---
Official Documentation
- WordPress Plugin Handbook: https://developer.wordpress.org/plugins/
- WordPress Coding Standards: https://developer.wordpress.org/coding-standards/
- WordPress REST API: https://developer.wordpress.org/rest-api/
- WordPress Database Class ($wpdb): https://developer.wordpress.org/reference/classes/wpdb/
- WordPress Security: https://developer.wordpress.org/apis/security/
- Settings API: https://developer.wordpress.org/plugins/settings/settings-api/
- Custom Post Types: https://developer.wordpress.org/plugins/post-types/
- Transients API: https://developer.wordpress.org/apis/transients/
- Context7 Library ID: /websites/developer_wordpress
---
Troubleshooting
Problem: Plugin causes fatal error
Solution: 1. Enable WP_DEBUG in wp-config.php 2. Check error log at wp-content/debug.log 3. Verify all class/function names are prefixed 4. Check for missing dependencies
Problem: 404 errors on custom post type pages
Solution: Flush rewrite rules
// Temporarily add to wp-admin
flush_rewrite_rules();
// Remove after visiting wp-admin onceProblem: Nonce verification always fails
Solution: 1. Check nonce name matches in field and verification 2. Verify using correct action name 3. Ensure nonce hasn't expired (24 hour default)
Problem: AJAX returns 0 or -1
Solution: 1. Verify action name matches hook: wp_ajax_{action} 2. Check nonce is being sent and verified 3. Ensure handler function exists and is hooked correctly
Problem: Sanitization stripping HTML
Solution: Use wp_kses_post() instead of sanitize_text_field() to allow safe HTML
Problem: Database queries not working
Solution: 1. Always use $wpdb->prepare() for queries with variables 2. Check table name includes $wpdb->prefix 3. Verify column names and syntax
---
Complete Setup Checklist
Use this checklist to verify your plugin:
- [ ] Plugin header complete with all fields
- [ ] ABSPATH check at top of every PHP file
- [ ] All functions/classes use unique prefix
- [ ] All forms have nonce verification
- [ ] All user input is sanitized
- [ ] All output is escaped
- [ ] All database queries use $wpdb->prepare()
- [ ] Capability checks (not just is_admin())
- [ ] Custom post types flush rewrite rules on activation
- [ ] Deactivation hook only clears temporary data
- [ ] uninstall.php handles permanent cleanup
- [ ] Text domain matches plugin slug
- [ ] Scripts/styles only load where needed
- [ ] WP_DEBUG enabled during development
- [ ] Tested with Query Monitor for performance
- [ ] No deprecated function warnings
- [ ] Works with latest WordPress version
---
Questions? Issues?
1. Check references/common-errors.md for extended troubleshooting 2. Verify all steps in the security foundation 3. Check official docs: https://developer.wordpress.org/plugins/ 4. Enable WP_DEBUG and check debug.log 5. Use Query Monitor plugin to debug hooks and queries
{
"name": "wordpress-plugin-core",
"description": "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.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"license": "MIT",
"repository": "https://github.com/jezweb/claude-skills",
"keywords": []
}
[TODO: Example Template File]
[TODO: This directory contains files that will be used in the OUTPUT that Claude produces.]
[TODO: Examples:]
- Templates (.html, .tsx, .md)
- Images (.png, .svg)
- Fonts (.ttf, .woff)
- Boilerplate code
- Configuration file templates
[TODO: Delete this file and add your actual assets]
These files are NOT loaded into context. They are copied or used directly in the final output.
<?php
/**
* Example: GitHub Auto-Updates
*
* This example demonstrates how to implement automatic updates from GitHub
* using the Plugin Update Checker library by YahnisElsts.
*
* Features:
* - Automatic updates from GitHub releases, tags, or branches
* - Support for public and private repositories
* - License key integration (optional)
* - Secure token storage
* - Error handling and fallbacks
*
* @package YourPlugin
* @see https://github.com/YahnisElsts/plugin-update-checker
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* ===================================================================
* Example 1: Basic GitHub Updates (Public Repository)
* ===================================================================
*
* Simplest implementation for public GitHub repositories.
*/
// Include Plugin Update Checker library
require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
// Initialize update checker
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__, // Full path to main plugin file
'your-plugin' // Plugin slug
);
// Optional: Set branch (default: master/main)
$updateChecker->setBranch( 'main' );
/**
* ===================================================================
* Example 2: GitHub Releases (Recommended)
* ===================================================================
*
* Use GitHub Releases for professional versioning with release notes.
* This downloads pre-built ZIP from releases instead of source code.
*/
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin'
);
// Download from releases instead of source
$updateChecker->getVcsApi()->enableReleaseAssets();
/**
* To create a release:
* 1. Update version in plugin header
* 2. Commit: git commit -m "Bump version to 1.0.1"
* 3. Tag: git tag 1.0.1 && git push origin 1.0.1
* 4. Create GitHub Release with pre-built ZIP (optional)
*/
/**
* ===================================================================
* Example 3: Private Repository with Authentication
* ===================================================================
*
* For private repositories, use a Personal Access Token.
*/
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/private-plugin/',
__FILE__,
'private-plugin'
);
// Set authentication token
$updateChecker->setAuthentication( 'ghp_YourGitHubPersonalAccessToken' );
/**
* SECURITY: Never hardcode tokens!
* Use wp-config.php constant instead:
*
* In wp-config.php:
* define( 'MY_PLUGIN_GITHUB_TOKEN', 'ghp_xxx' );
*
* In plugin:
* if ( defined( 'MY_PLUGIN_GITHUB_TOKEN' ) ) {
* $updateChecker->setAuthentication( MY_PLUGIN_GITHUB_TOKEN );
* }
*/
/**
* ===================================================================
* Example 4: Complete Implementation with Best Practices
* ===================================================================
*
* Production-ready implementation with error handling, caching,
* and optional license integration.
*/
/**
* Initialize GitHub auto-updates
*/
function yourprefix_init_github_updates() {
// Path to update checker library
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
// Check if library exists
if ( ! file_exists( $updater_path ) ) {
add_action( 'admin_notices', 'yourprefix_update_checker_missing_notice' );
return;
}
require $updater_path;
// Initialize update checker
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin'
);
// Set branch
$updateChecker->setBranch( 'main' );
// Use GitHub Releases
$updateChecker->getVcsApi()->enableReleaseAssets();
// Private repo authentication (from wp-config.php)
if ( defined( 'YOURPREFIX_GITHUB_TOKEN' ) ) {
$updateChecker->setAuthentication( YOURPREFIX_GITHUB_TOKEN );
}
// Optional: License-based updates
$license_key = get_option( 'yourprefix_license_key' );
if ( ! empty( $license_key ) && yourprefix_validate_license( $license_key ) ) {
// Use license key as authentication token
$updateChecker->setAuthentication( $license_key );
}
// Optional: Custom update checks
add_filter( 'puc_request_info_result-your-plugin', 'yourprefix_filter_update_checks', 10, 2 );
}
add_action( 'plugins_loaded', 'yourprefix_init_github_updates' );
/**
* Admin notice if update checker library is missing
*/
function yourprefix_update_checker_missing_notice() {
?>
<div class="notice notice-error">
<p>
<strong><?php esc_html_e( 'Your Plugin:', 'your-plugin' ); ?></strong>
<?php esc_html_e( 'Update checker library not found. Automatic updates are disabled.', 'your-plugin' ); ?>
</p>
</div>
<?php
}
/**
* Validate license key (example implementation)
*
* @param string $license_key License key to validate.
* @return bool True if valid, false otherwise.
*/
function yourprefix_validate_license( $license_key ) {
// Check cached validation result
$cached = get_transient( 'yourprefix_license_valid_' . md5( $license_key ) );
if ( false !== $cached ) {
return (bool) $cached;
}
// Validate with your license server
$response = wp_remote_post(
'https://example.com/api/validate-license',
array(
'body' => array(
'license' => sanitize_text_field( $license_key ),
'domain' => home_url(),
'product' => 'your-plugin',
),
)
);
if ( is_wp_error( $response ) ) {
return false;
}
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( ! $body || ! isset( $body->valid ) ) {
return false;
}
$is_valid = (bool) $body->valid;
// Cache for 24 hours
set_transient( 'yourprefix_license_valid_' . md5( $license_key ), $is_valid, DAY_IN_SECONDS );
return $is_valid;
}
/**
* Filter update checks (optional)
*
* Allows custom logic before updates are offered.
*
* @param object $info Update information from GitHub.
* @param object $result Response from GitHub API.
* @return object Modified update information.
*/
function yourprefix_filter_update_checks( $info, $result ) {
// Example: Block updates if license is invalid
$license_key = get_option( 'yourprefix_license_key' );
if ( empty( $license_key ) || ! yourprefix_validate_license( $license_key ) ) {
return null; // Don't show update
}
// Example: Add custom data
if ( $info ) {
$info->tested = '6.4'; // Override "Tested up to" version
}
return $info;
}
/**
* ===================================================================
* Example 5: Multiple Update Channels (Stable + Beta)
* ===================================================================
*
* Offer beta updates to users who opt in.
*/
function yourprefix_init_multi_channel_updates() {
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
if ( ! file_exists( $updater_path ) ) {
return;
}
require $updater_path;
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin'
);
// Check if user opted into beta updates
$beta_enabled = get_option( 'yourprefix_enable_beta_updates', false );
if ( $beta_enabled ) {
// Use beta branch
$updateChecker->setBranch( 'beta' );
} else {
// Use stable releases
$updateChecker->setBranch( 'main' );
$updateChecker->getVcsApi()->enableReleaseAssets();
}
}
add_action( 'plugins_loaded', 'yourprefix_init_multi_channel_updates' );
/**
* Settings field for beta opt-in
*/
function yourprefix_add_beta_settings() {
add_settings_field(
'yourprefix_enable_beta',
__( 'Enable Beta Updates', 'your-plugin' ),
'yourprefix_render_beta_field',
'your-plugin-settings',
'yourprefix_general_section'
);
}
add_action( 'admin_init', 'yourprefix_add_beta_settings' );
/**
* Render beta updates checkbox
*/
function yourprefix_render_beta_field() {
$enabled = get_option( 'yourprefix_enable_beta_updates', false );
?>
<label>
<input type="checkbox" name="yourprefix_enable_beta_updates" value="1" <?php checked( $enabled, true ); ?>>
<?php esc_html_e( 'Receive beta updates (may be unstable)', 'your-plugin' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Enable this to test new features before stable release.', 'your-plugin' ); ?>
</p>
<?php
}
/**
* ===================================================================
* Example 6: GitLab Support
* ===================================================================
*
* Plugin Update Checker also supports GitLab and Bitbucket.
*/
$updateChecker = PucFactory::buildUpdateChecker(
'https://gitlab.com/yourusername/your-plugin',
__FILE__,
'your-plugin'
);
// GitLab authentication
$updateChecker->setAuthentication( 'your-gitlab-private-token' );
/**
* ===================================================================
* Example 7: Custom JSON Update Server
* ===================================================================
*
* Use a custom update server with JSON endpoint.
*/
$updateChecker = PucFactory::buildUpdateChecker(
'https://example.com/updates/your-plugin.json',
__FILE__,
'your-plugin'
);
/**
* JSON format:
* {
* "version": "1.0.1",
* "download_url": "https://example.com/downloads/your-plugin-1.0.1.zip",
* "sections": {
* "description": "Plugin description",
* "changelog": "<h4>1.0.1</h4><ul><li>Bug fixes</li></ul>"
* },
* "tested": "6.4",
* "requires": "5.9",
* "requires_php": "7.4"
* }
*/
/**
* ===================================================================
* Example 8: Logging and Debugging
* ===================================================================
*
* Enable logging for troubleshooting update issues.
*/
function yourprefix_init_updates_with_logging() {
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
if ( ! file_exists( $updater_path ) ) {
return;
}
require $updater_path;
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin'
);
// Enable debug mode (logs to error_log)
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// Log all API requests
add_action(
'puc_api_request_start',
function ( $url, $args ) {
error_log( sprintf( '[Plugin Updates] Checking: %s', $url ) );
},
10,
2
);
// Log API responses
add_action(
'puc_api_request_end',
function ( $response, $url ) {
if ( is_wp_error( $response ) ) {
error_log( sprintf( '[Plugin Updates] Error: %s', $response->get_error_message() ) );
} else {
error_log( sprintf( '[Plugin Updates] Success: %d bytes received', strlen( wp_remote_retrieve_body( $response ) ) ) );
}
},
10,
2
);
}
}
add_action( 'plugins_loaded', 'yourprefix_init_updates_with_logging' );
/**
* ===================================================================
* Example 9: Rate Limiting Update Checks
* ===================================================================
*
* Prevent excessive API calls to GitHub.
*/
function yourprefix_init_rate_limited_updates() {
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
if ( ! file_exists( $updater_path ) ) {
return;
}
require $updater_path;
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin'
);
// Set custom check period (in hours)
$updateChecker->setCheckPeriod( 12 ); // Check every 12 hours instead of default
}
add_action( 'plugins_loaded', 'yourprefix_init_rate_limited_updates' );
/**
* ===================================================================
* Example 10: Cleanup on Uninstall
* ===================================================================
*
* Remove update checker transients when plugin is uninstalled.
*/
// In uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete update checker transients
global $wpdb;
// Delete all transients for this plugin
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
'%puc_update_cache_your-plugin%'
)
);
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
'%puc_cron_check_your-plugin%'
)
);
/**
* ===================================================================
* Security Notes
* ===================================================================
*
* 1. ALWAYS use HTTPS for repository URLs
* 2. NEVER hardcode authentication tokens in plugin code
* 3. Store tokens in wp-config.php or encrypt them
* 4. Implement license validation before offering updates
* 5. Use checksums to verify downloaded files (see references/github-auto-updates.md)
* 6. Rate limit update checks to avoid API throttling
* 7. Log errors for debugging but don't expose sensitive data
* 8. Clear cached update data after installation
*/
/**
* ===================================================================
* Installation Checklist
* ===================================================================
*
* 1. Install Plugin Update Checker library:
* cd your-plugin/
* git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
*
* 2. Add initialization code (see examples above)
*
* 3. For private repos, add token to wp-config.php:
* define( 'YOUR_PLUGIN_GITHUB_TOKEN', 'ghp_xxx' );
*
* 4. Test by creating a new tag on GitHub:
* git tag 1.0.1
* git push origin 1.0.1
*
* 5. Check for updates in WordPress admin:
* Dashboard → Updates → Should show your plugin
*
* 6. (Optional) Create GitHub Release for better UX:
* - Go to GitHub → Releases → Create Release
* - Upload pre-built ZIP (without .git, tests, etc.)
* - Add release notes
*/
/**
* ===================================================================
* Troubleshooting
* ===================================================================
*
* Updates not showing?
* 1. Check plugin version in header matches current version
* 2. Verify GitHub repository URL is correct
* 3. Ensure authentication token is valid (for private repos)
* 4. Check WordPress debug log for errors
* 5. Manually clear transients: delete_site_transient( 'update_plugins' )
* 6. Verify GitHub has releases or tags
*
* Wrong version downloaded?
* 1. Ensure you're using git tags or GitHub Releases
* 2. Check branch setting matches your repository
* 3. Verify version numbers use semantic versioning (1.0.0, not v1.0.0)
*
* Installation fails?
* 1. Verify ZIP structure includes plugin folder inside ZIP
* 2. Check file permissions on server
* 3. Ensure no syntax errors in updated files
* 4. Check WordPress debug log for specific error messages
*/
/**
* ===================================================================
* Additional Resources
* ===================================================================
*
* - Plugin Update Checker Documentation:
* https://github.com/YahnisElsts/plugin-update-checker
*
* - Complete guide with security best practices:
* See references/github-auto-updates.md
*
* - WordPress Plugin API:
* https://developer.wordpress.org/plugins/
*
* - GitHub Personal Access Tokens:
* https://github.com/settings/tokens
*/
WordPress Plugin Development (Core)
Status: Production Ready ✅ Last Updated: 2025-11-06 Production Tested: Based on WordPress Plugin Handbook official documentation + Patchstack Security Database
---
Auto-Trigger Keywords
Claude Code automatically discovers this skill when you mention:
Primary Keywords
- wordpress plugin
- wordpress plugin development
- wp plugin development
- wordpress coding standards
- wordpress plugin architecture
Secondary Keywords
- wordpress security
- wordpress hooks
- wordpress filters
- custom post type
- register_post_type
- register_taxonomy
- wordpress settings api
- wordpress rest api
- admin-ajax
- add_meta_box
- add_options_page
- register_rest_route
- $wpdb
- wpdb prepare
Security Keywords
- sanitize_text_field
- esc_html
- esc_attr
- esc_url
- wp_kses_post
- wp_nonce
- wp_verify_nonce
- wp_nonce_field
- check_ajax_referer
- current_user_can
Distribution & Updates Keywords
- github auto-updates
- github updates
- plugin auto-update
- plugin update checker
- wordpress plugin distribution
- git updater
- custom update server
- plugin versioning
- github releases
- private plugin updates
- license key updates
- plugin update api
- wordpress transients updates
Error-Based Keywords
- "wordpress sql injection"
- "wordpress xss"
- "wordpress csrf"
- "plugin activation 404"
- "nonce verification failed"
- "wordpress security vulnerability"
- "wordpress sanitization"
- "wordpress escaping"
- "plugin naming conflict"
- "custom post type 404"
---
What This Skill Does
This skill provides comprehensive knowledge for building secure, standards-compliant WordPress plugins. It covers core patterns, security best practices, database interactions, hooks/filters, Settings API, custom post types, REST API, and AJAX implementations.
Core Capabilities
✅ Security Foundation - Prevents 20+ documented vulnerabilities (SQL injection, XSS, CSRF, etc.) ✅ Plugin Architecture - Simple, OOP, and PSR-4 patterns with templates ✅ WordPress APIs - Settings API, REST API, Custom Post Types, Taxonomies, Meta Boxes ✅ Database Patterns - Secure $wpdb queries, custom tables, transients ✅ Standards Compliance - WordPress Coding Standards, prefixing, ABSPATH checks ✅ Lifecycle Management - Activation, deactivation, uninstall hooks ✅ Distribution & Updates - GitHub auto-updates, Plugin Update Checker, versioning, releases ✅ Advanced Features - WP-CLI commands, scheduled events, internationalization
---
Known Issues This Skill Prevents
| Issue | Why It Happens | Source | How Skill Fixes It |
|---|---|---|---|
| SQL Injection (15%) | Direct concatenation of user input | Patchstack | Always use $wpdb->prepare() with placeholders |
| XSS (35%) | Unsanitized output to HTML | Patchstack DB | Escape all output with esc_html(), esc_attr(), etc. |
| CSRF (10-15%) | No request origin verification | NinTechNet | Use nonces with wp_verify_nonce() |
| Missing Capability Checks | Using is_admin() instead of current_user_can() | WP Security Guidelines | Always check capabilities |
| Direct File Access | No ABSPATH check | WP Plugin Handbook | Add ABSPATH check to every file |
| Prefix Collision | Generic function/class names | WP Coding Standards | Use unique 4-5 char prefix |
| 404 on Custom Post Types | Rewrite rules not flushed | WP Plugin Handbook | Flush on activation |
| Transient Accumulation | No cleanup on uninstall | WP Transients API | Delete in uninstall.php |
| Performance Issues | Scripts loaded everywhere | WP Performance Best Practices | Conditional asset enqueuing |
| Data Loss on Deactivation | Deleting data on deactivation | WP Best Practices | Only delete in uninstall.php |
Total: 20 documented issues prevented
---
When to Use This Skill
✅ Use When:
- Creating new WordPress plugins from scratch
- Implementing security features (nonces, sanitization, escaping)
- Working with WordPress database ($wpdb, custom tables)
- Building admin interfaces (Settings API, meta boxes)
- Registering custom post types or taxonomies
- Creating REST API endpoints
- Handling AJAX requests
- Debugging plugin activation/deactivation issues
- Preventing security vulnerabilities
- Setting up auto-updates from GitHub or custom servers
- Distributing plugins outside WordPress.org
- Implementing license key validation for premium plugins
❌ Don't Use When:
- Building Gutenberg blocks → Use
wordpress-gutenberg-blocksskill - Creating WooCommerce extensions → Use
woocommerce-extensionskill - Developing Gravity Forms add-ons → Use
gravity-forms-addonskill - Building Elementor widgets → Use
elementor-widgetskill
Claude Code will automatically combine this skill with specialized skills when needed.
---
Quick Usage Example
# 1. Copy plugin template
cp -r templates/plugin-psr4/ ~/wp-content/plugins/my-plugin/
# 2. Install Composer dependencies (if using PSR-4)
cd ~/wp-content/plugins/my-plugin/
composer install
# 3. Activate plugin
wp plugin activate my-pluginResult: Secure, standards-compliant WordPress plugin ready for development
Full instructions: See SKILL.md
---
Token Efficiency Metrics
| Approach | Tokens Used | Errors Encountered | Time to Complete |
|---|---|---|---|
| Manual Setup | ~15,000 | 2-4 | ~30 min |
| With This Skill | ~5,000 | 0 ✅ | ~10 min |
| Savings | ~67% | 100% | ~67% |
---
Package Versions (Verified 2025-11-06)
| Package | Version | Status |
|---|---|---|
| WordPress | 6.7+ | ✅ Latest stable |
| PHP | 7.4+ (8.0+ recommended) | ✅ Current |
| Composer | 2.0+ (optional) | ✅ Latest |
| WP-CLI | 2.0+ (optional) | ✅ Latest |
---
Dependencies
Prerequisites: None
Integrates With:
wordpress-gutenberg-blocks(for block development)woocommerce-extension(for WooCommerce plugins)gravity-forms-addon(for Gravity Forms add-ons)elementor-widget(for Elementor widgets)
---
File Structure
wordpress-plugin-core/
├── SKILL.md # Complete documentation (1,400+ lines)
├── README.md # This file
├── templates/
│ ├── plugin-simple/ # Simple functional plugin
│ ├── plugin-oop/ # Object-oriented plugin
│ ├── plugin-psr4/ # Modern PSR-4 plugin with Composer
│ └── examples/ # Meta boxes, settings, REST, AJAX
├── scripts/
│ ├── scaffold-plugin.sh # Interactive plugin scaffolding
│ ├── check-security.sh # Security audit tool
│ └── validate-headers.sh # Plugin header validator
├── references/
│ ├── security-checklist.md # Complete security audit
│ ├── hooks-reference.md # Common WordPress hooks
│ ├── sanitization-guide.md # All sanitization functions
│ ├── wpdb-patterns.md # Database query patterns
│ └── common-errors.md # Extended error documentation
└── assets/
└── .gitignore # Ignore vendor/, node_modules/---
Quick Reference
The 5-Step Security Foundation
// 1. Unique Prefix (4-5 chars)
function mypl_init() {}
// 2. ABSPATH Check (every PHP file)
if ( ! defined( 'ABSPATH' ) ) exit;
// 3. Sanitize Input, Escape Output
$clean = sanitize_text_field( $_POST['input'] );
echo esc_html( $output );
// 4. Nonces (CSRF Protection)
wp_nonce_field( 'mypl_action', 'mypl_nonce' );
wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' );
// 5. Prepared Statements (SQL Injection Prevention)
$wpdb->prepare( "SELECT * FROM table WHERE id = %d", $id );Plugin Header
<?php
/**
* Plugin Name: My Awesome Plugin
* Plugin URI: https://example.com/my-plugin/
* Description: Brief description of what this 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
* Text Domain: my-plugin
*/
if ( ! defined( 'ABSPATH' ) ) exit;Custom Post Type
function mypl_register_cpt() {
register_post_type( 'book', array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
),
'public' => true,
'has_archive' => true,
'show_in_rest' => true,
'supports' => array( 'title', 'editor', 'thumbnail' ),
) );
}
add_action( 'init', 'mypl_register_cpt' );
// CRITICAL: Flush on activation
register_activation_hook( __FILE__, function() {
mypl_register_cpt();
flush_rewrite_rules();
} );REST API Endpoint
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'GET',
'callback' => 'mypl_rest_callback',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => 'is_numeric',
'sanitize_callback' => 'absint',
),
),
) );
} );---
Official Documentation
- WordPress Plugin Handbook: https://developer.wordpress.org/plugins/
- WordPress Coding Standards: https://developer.wordpress.org/coding-standards/
- WordPress Security: https://developer.wordpress.org/apis/security/
- $wpdb Class Reference: https://developer.wordpress.org/reference/classes/wpdb/
- WordPress REST API: https://developer.wordpress.org/rest-api/
- Context7 Library: /websites/developer_wordpress
---
Related Skills
- wordpress-gutenberg-blocks - Gutenberg block development (React, @wordpress/create-block)
- woocommerce-extension - WooCommerce-specific hooks, product types, payment gateways
- gravity-forms-addon - GFAddOn class, feed integrations, custom fields
- elementor-widget - Elementor widget registration, controls, rendering
---
Contributing
Found an issue or have a suggestion?
- Open an issue: https://github.com/jezweb/claude-skills/issues
- See SKILL.md for detailed documentation
---
License
MIT License - See main repo LICENSE file
---
Production Tested: Based on official WordPress documentation + Patchstack Security Database Token Savings: ~67% (15k → 5k tokens) Error Prevention: 100% (20 documented issues prevented) Ready to use! See SKILL.md for complete setup.
Common WordPress Hooks Reference
Quick reference for the most commonly used WordPress hooks in plugin development.
---
Action Hooks
Plugin Lifecycle
| Hook | When It Fires | Use For |
|---|---|---|
plugins_loaded | After all plugins loaded | Init plugin functionality |
init | WordPress initialization | Register post types, taxonomies |
admin_init | Admin initialization | Register settings |
wp_loaded | WordPress fully loaded | Late initialization |
Admin Hooks
| Hook | When It Fires | Use For |
|---|---|---|
admin_menu | Admin menu creation | Add admin pages |
admin_enqueue_scripts | Admin assets loading | Enqueue admin CSS/JS |
admin_notices | Admin notices display | Show admin messages |
save_post | After post saved | Save custom data |
add_meta_boxes | Meta boxes registration | Add meta boxes |
Frontend Hooks
| Hook | When It Fires | Use For |
|---|---|---|
wp_enqueue_scripts | Frontend assets loading | Enqueue CSS/JS |
wp_head | In <head> section | Add meta tags, styles |
wp_footer | Before </body> | Add scripts, analytics |
template_redirect | Before template loaded | Redirects, custom templates |
the_content (filter) | Post content display | Modify post content |
AJAX Hooks
| Hook | Use For |
|---|---|
wp_ajax_{action} | Logged-in AJAX |
wp_ajax_nopriv_{action} | Public AJAX |
REST API
| Hook | When It Fires | Use For |
|---|---|---|
rest_api_init | REST API init | Register REST routes |
---
Filter Hooks
Content Filters
| Hook | What It Filters | Common Use |
|---|---|---|
the_content | Post content | Add/modify content |
the_title | Post title | Modify titles |
the_excerpt | Post excerpt | Customize excerpts |
comment_text | Comment text | Modify comments |
Query Filters
| Hook | What It Filters | Common Use |
|---|---|---|
pre_get_posts | Query before execution | Modify queries |
posts_where | SQL WHERE clause | Custom WHERE |
posts_orderby | SQL ORDER BY | Custom sorting |
Admin Filters
| Hook | What It Filters | Common Use |
|---|---|---|
manage_{post_type}_posts_columns | Admin columns | Add columns |
admin_footer_text | Admin footer text | Custom footer |
---
Hook Priority
Default priority is 10. Lower numbers run first.
// Runs early (priority 5)
add_action( 'init', 'my_function', 5 );
// Runs late (priority 20)
add_action( 'init', 'my_other_function', 20 );---
Resources
- Hook Reference: https://developer.wordpress.org/reference/hooks/
- Plugin API: https://codex.wordpress.org/Plugin_API
[TODO: Reference Document Name]
[TODO: This file contains reference documentation that Claude can load when needed.]
[TODO: Delete this file if you don't have reference documentation to provide.]
Purpose
[TODO: Explain what information this document contains]
When Claude Should Use This
[TODO: Describe specific scenarios where Claude should load this reference]
Content
[TODO: Add your reference content here - schemas, guides, specifications, etc.]
---
Note: This file is NOT loaded into context by default. Claude will only load it when:
- It determines the information is needed
- You explicitly ask Claude to reference it
- The SKILL.md instructions direct Claude to read it
Keep this file under 10k words for best performance.
GitHub Auto-Updates for WordPress Plugins
Complete guide for implementing automatic updates for WordPress plugins hosted outside the WordPress.org repository.
---
Table of Contents
1. Overview 2. How WordPress Updates Work 3. Solution 1: Plugin Update Checker (Recommended) 4. Solution 2: Git Updater 5. Solution 3: Custom Update Server 6. Commercial Solutions 7. Security Best Practices 8. Comparison Matrix 9. Common Pitfalls 10. Resources
---
Overview
WordPress plugins don't need to be hosted on WordPress.org to provide automatic updates. Several well-established solutions enable auto-updates from GitHub, GitLab, BitBucket, or custom servers.
Why Auto-Updates Matter
- Security: Users get critical security patches automatically
- Features: Seamless delivery of new features and improvements
- Support: Reduces support burden from outdated installations
- Professional: Provides polished user experience
Solutions Available
| Solution | Best For | Complexity | Cost |
|---|---|---|---|
| Plugin Update Checker | Most use cases | Low | Free |
| Git Updater | Multi-platform Git hosting | Low | Free |
| Custom Update Server | Enterprise/custom licensing | High | Hosting costs |
| Freemius | Commercial plugins | Low | 15% or $99-599/yr |
---
How WordPress Updates Work
Core Update Mechanism
WordPress uses a transient-based system to check for plugin updates:
1. WordPress checks transients (every 12 hours)
2. Queries WordPress.org API for available updates
3. Stores results in `update_plugins` transient
4. Displays "Update available" notifications
5. Downloads and installs when user clicks "Update"Key Hooks for Custom Updates
// Intercept BEFORE WordPress checks (saves to transient)
add_filter('pre_set_site_transient_update_plugins', 'my_check_for_updates');
// Filter update data (returns without saving)
add_filter('site_transient_update_plugins', 'my_push_update');
// Modify plugin information for "View details" modal
add_filter('plugins_api', 'my_plugin_info', 20, 3);
// Post-installation cleanup
add_action('upgrader_post_install', 'my_post_install', 10, 3);Update Flow for Custom Plugins
1. WordPress checks transients
↓
2. Your filter hook intercepts
↓
3. You query GitHub/custom server
↓
4. Inject update data into transient
↓
5. WordPress shows "Update available"
↓
6. User clicks update
↓
7. WordPress downloads from your URL
↓
8. Installs and activates---
Solution 1: Plugin Update Checker (Recommended)
GitHub: https://github.com/YahnisElsts/plugin-update-checker Stars: 2.2k+ | License: MIT | Status: Actively maintained (v5.x)
What It Does
Lightweight library that enables automatic updates from GitHub, GitLab, BitBucket, or custom JSON servers. No WordPress.org submission required.
Pros
- ✅ Minimal setup: ~5 lines of code
- ✅ Multiple platforms: GitHub, GitLab, BitBucket, custom JSON
- ✅ Active development: Regular updates since 2011
- ✅ Private repos: Supports authentication tokens
- ✅ Well documented: Extensive examples and guides
- ✅ No dependencies: Self-contained library
- ✅ Flexible versioning: Supports releases, tags, or branches
- ✅ Standards compliant: Uses WordPress.org
readme.txtformat
Cons
- ❌ No built-in license management (requires custom implementation)
- ❌ No package signing (relies on HTTPS/token security)
- ❌ Requires bundling library with plugin (~100KB)
Installation
Option A: Git Submodule (Recommended)
cd your-plugin/
git submodule add https://github.com/YahnisElsts/plugin-update-checker.gitOption B: Composer
composer require yahnis-elsts/plugin-update-checkerOption C: Manual
cd your-plugin/
git clone https://github.com/YahnisElsts/plugin-update-checker.git
# Or download and extract ZIPBasic Implementation
<?php
/**
* Plugin Name: My Awesome Plugin
* Version: 1.0.0
* GitHub Plugin URI: https://github.com/yourusername/your-plugin
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Include the library
require 'plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
// Initialize update checker
$myUpdateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'my-awesome-plugin' // Plugin slug
);
// Optional: Set branch (default: master/main)
$myUpdateChecker->setBranch('main');Private Repository Support
// For private GitHub repos, add authentication
$myUpdateChecker->setAuthentication('ghp_YourGitHubPersonalAccessToken');
// Better: Use WordPress constant (define in wp-config.php)
if (defined('MY_PLUGIN_GITHUB_TOKEN')) {
$myUpdateChecker->setAuthentication(MY_PLUGIN_GITHUB_TOKEN);
}Release Strategies
Strategy 1: GitHub Releases (Recommended)
Best for: Production plugins with formal releases
# 1. Update version in plugin header
# my-plugin.php: Version: 1.0.1
# 2. Commit and tag
git add my-plugin.php
git commit -m "Bump version to 1.0.1"
git tag 1.0.1
git push origin main
git push origin 1.0.1
# 3. Create GitHub Release
# - Go to GitHub → Releases → Create Release
# - Select tag: 1.0.1
# - Upload pre-built plugin ZIP (optional but recommended)Enable release assets:
// Download ZIP from releases instead of source code
$myUpdateChecker->getVcsApi()->enableReleaseAssets();Benefits:
- ✅ Professional release notes
- ✅ Pre-built ZIP (can include compiled assets)
- ✅ Changelog visible to users
- ✅ Can exclude dev files (.git, tests, etc.)
Strategy 2: Git Tags
Best for: Simple plugins, rapid iteration
# Just tag the commit
git tag 1.0.1
git push origin 1.0.1No additional code needed - library auto-detects highest version tag.
Benefits:
- ✅ Simple workflow
- ✅ No manual release creation
Drawbacks:
- ❌ Downloads entire repo (includes .git, tests, etc.)
- ❌ No release notes visible to users
Strategy 3: Branch-Based
Best for: Beta testing, staging environments
// Point to specific branch
$myUpdateChecker->setBranch('stable');
// Or beta branch
$myUpdateChecker->setBranch('beta');Update version in plugin header on that branch:
/**
* Version: 1.1.0-beta
*/Benefits:
- ✅ Easy beta testing
- ✅ Separate stable/development channels
Drawbacks:
- ❌ No version history
- ❌ Downloads full repo
Complete Example with Best Practices
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: Example plugin with GitHub updates
* Version: 1.0.0
* Author: Your Name
* Author URI: https://example.com
* License: GPL-2.0+
* Text Domain: my-plugin
* GitHub Plugin URI: https://github.com/yourusername/my-plugin
*/
if (!defined('ABSPATH')) {
exit;
}
// Define constants
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_FILE', __FILE__);
define('MY_PLUGIN_DIR', plugin_dir_path(__FILE__));
// Initialize update checker
add_action('plugins_loaded', 'my_plugin_init_updater');
function my_plugin_init_updater() {
// Only load if library exists
$updater_path = MY_PLUGIN_DIR . 'plugin-update-checker/plugin-update-checker.php';
if (!file_exists($updater_path)) {
// Warn admin if library is missing
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>';
echo '<strong>My Plugin:</strong> Update checker library not found. ';
echo 'Automatic updates disabled.';
echo '</p></div>';
});
return;
}
require $updater_path;
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://github.com/yourusername/my-plugin/',
MY_PLUGIN_FILE,
'my-plugin'
);
// Set branch
$updateChecker->setBranch('main');
// Use GitHub Releases
$updateChecker->getVcsApi()->enableReleaseAssets();
// Private repo authentication (optional)
if (defined('MY_PLUGIN_GITHUB_TOKEN')) {
$updateChecker->setAuthentication(MY_PLUGIN_GITHUB_TOKEN);
}
// Add custom authentication from settings (optional)
$license_key = get_option('my_plugin_license_key');
if (!empty($license_key)) {
// Use license key as token or validate separately
$updateChecker->setAuthentication($license_key);
}
}
// Rest of plugin code...Creating Update-Ready ZIP Files
Important: ZIP must contain plugin folder inside it!
my-plugin-1.0.1.zip
└── my-plugin/ ← Plugin folder MUST be inside
├── my-plugin.php
├── readme.txt
├── plugin-update-checker/
└── ...Build script example:
#!/bin/bash
# build-release.sh
VERSION="1.0.1"
PLUGIN_SLUG="my-plugin"
# Create temp directory
mkdir -p build
# Export git repository (excludes .git, .gitignore, etc.)
git archive HEAD --prefix="${PLUGIN_SLUG}/" --format=zip -o "build/${PLUGIN_SLUG}-${VERSION}.zip"
echo "Built: build/${PLUGIN_SLUG}-${VERSION}.zip"With exclusions:
# .gitattributes
.git export-ignore
.gitignore export-ignore
.gitattributes export-ignore
tests/ export-ignore
node_modules/ export-ignore
src/ export-ignore
.github/ export-ignorereadme.txt Format
Plugin Update Checker reads readme.txt for changelog and upgrade notices:
=== My Plugin ===
Contributors: yourusername
Tags: feature, awesome
Requires at least: 5.9
Tested up to: 6.4
Stable tag: 1.0.1
License: GPLv2 or later
Short description here.
== Description ==
Long description here.
== Changelog ==
= 1.0.1 =
* Fixed bug with user permissions
* Added new feature X
= 1.0.0 =
* Initial release
== Upgrade Notice ==
= 1.0.1 =
Critical security fix. Update immediately.---
Solution 2: Git Updater
GitHub: https://github.com/afragen/git-updater Stars: 3.3k+ | License: MIT | Status: Actively maintained
What It Does
A WordPress plugin (not library) that enables updates for all GitHub/GitLab/Bitbucket/Gitea-hosted plugins and themes on a site.
Pros
- ✅ User-installable: Site owners install once, works for all compatible plugins
- ✅ Multi-platform: GitHub, GitLab, Bitbucket, Gitea
- ✅ No coding required: Just add headers to plugin
- ✅ Language pack support: Automatic translations
- ✅ REST API: Programmatic control
Cons
- ❌ Dependency: Users must install Git Updater plugin first
- ❌ Less control: Developer doesn't control update logic
- ❌ PHP 8.0+ required: May limit compatibility
Implementation
Step 1: Add headers to your plugin
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com
* Description: My awesome plugin
* Version: 1.0.0
* Author: Your Name
* GitHub Plugin URI: yourusername/your-plugin
* Primary Branch: main
* Requires at least: 5.9
* Requires PHP: 8.0
*/Alternative formats:
// Full URL
GitHub Plugin URI: https://github.com/yourusername/your-plugin
// GitLab
GitLab Plugin URI: yourusername/your-plugin
// Bitbucket
Bitbucket Plugin URI: yourusername/your-plugin
// Gitea
Gitea Plugin URI: https://gitea.example.com/yourusername/your-pluginStep 2: Users install Git Updater
Users download Git Updater from WordPress.org:
Plugins → Add New → Search "Git Updater" → Install & ActivateStep 3: Install your plugin
Users can install via:
- Upload ZIP
- Git Updater → Install Plugin → Enter GitHub URL
Step 4: Updates appear automatically
Git Updater checks for updates alongside WordPress.org plugins.
For Private Repos
GitHub (Settings → Git Updater → GitHub → Personal Access Token):
Token: ghp_xxxxxxxxxxxxxOr in wp-config.php:
define('GITHUB_ACCESS_TOKEN', 'ghp_xxxxxxxxxxxxx');When to Use
- ✅ Building plugins for clients who already use Git Updater
- ✅ Want to offload update logic to third-party
- ✅ Need language pack support
- ✅ Multi-platform Git hosting
- ❌ Don't want user dependencies
---
Solution 3: Custom Update Server
GitHub: https://github.com/YahnisElsts/wp-update-server (companion library) Tutorial: https://rudrastyh.com/wordpress/self-hosted-plugin-update.html
What It Does
Roll your own update API using WordPress filters and custom JSON/API endpoint.
Pros
- ✅ Full control: Complete customization of update logic
- ✅ No dependencies: No external libraries
- ✅ Scalable: Host on CDN/S3
- ✅ Integration-friendly: Easy to add license checks, analytics
Cons
- ❌ More code: Requires implementing API and plugin-side logic
- ❌ Maintenance: You're responsible for security, uptime
- ❌ Hosting costs: Need server/CDN
Implementation
Server Setup
Create `info.json` endpoint:
{
"name": "My Plugin",
"slug": "my-plugin",
"version": "1.0.1",
"author": "Your Name",
"homepage": "https://example.com",
"download_url": "https://example.com/downloads/my-plugin-1.0.1.zip",
"requires": "5.0",
"tested": "6.4",
"requires_php": "7.4",
"last_updated": "2025-01-15 12:00:00",
"sections": {
"description": "Plugin description here",
"installation": "<h4>Installation</h4><ol><li>Upload plugin</li></ol>",
"changelog": "<h4>1.0.1</h4><ul><li>Bug fixes</li></ul>"
},
"banners": {
"low": "https://example.com/banner-772x250.png",
"high": "https://example.com/banner-1544x500.png"
},
"icons": {
"1x": "https://example.com/icon-128x128.png",
"2x": "https://example.com/icon-256x256.png"
}
}Plugin Code
<?php
/**
* Plugin Name: My Plugin
* Version: 1.0.0
* Update URI: https://example.com/updates/my-plugin.json
*/
if (!defined('ABSPATH')) {
exit;
}
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_UPDATE_URL', 'https://example.com/updates/my-plugin.json');
/**
* Check for plugin updates
*/
add_filter('site_transient_update_plugins', 'my_plugin_check_for_updates');
function my_plugin_check_for_updates($transient) {
if (empty($transient->checked)) {
return $transient;
}
$plugin_slug = plugin_basename(__FILE__);
// Check cache first (12 hours)
$remote_data = get_transient('my_plugin_update_cache');
if (false === $remote_data) {
$remote = wp_remote_get(MY_PLUGIN_UPDATE_URL, [
'timeout' => 10,
'headers' => [
'Accept' => 'application/json'
]
]);
if (is_wp_error($remote) || 200 !== wp_remote_retrieve_response_code($remote)) {
return $transient;
}
$remote_data = json_decode(wp_remote_retrieve_body($remote));
if (!$remote_data || !isset($remote_data->version)) {
return $transient;
}
// Cache for 12 hours
set_transient('my_plugin_update_cache', $remote_data, 12 * HOUR_IN_SECONDS);
}
// Compare versions
if (version_compare(MY_PLUGIN_VERSION, $remote_data->version, '<')) {
$obj = new stdClass();
$obj->slug = $remote_data->slug;
$obj->plugin = $plugin_slug;
$obj->new_version = $remote_data->version;
$obj->url = $remote_data->homepage;
$obj->package = $remote_data->download_url;
$obj->tested = $remote_data->tested;
$obj->requires = $remote_data->requires;
$obj->requires_php = $remote_data->requires_php;
$transient->response[$plugin_slug] = $obj;
} else {
// Mark as up-to-date
$obj = new stdClass();
$obj->slug = $remote_data->slug;
$obj->plugin = $plugin_slug;
$obj->new_version = $remote_data->version;
$transient->no_update[$plugin_slug] = $obj;
}
return $transient;
}
/**
* Plugin information for "View details" modal
*/
add_filter('plugins_api', 'my_plugin_info', 20, 3);
function my_plugin_info($res, $action, $args) {
// Do nothing if not getting plugin information
if ('plugin_information' !== $action) {
return $res;
}
// Do nothing if it's not our plugin
if (plugin_basename(__DIR__) !== $args->slug) {
return $res;
}
// Try to get cached data
$remote_data = get_transient('my_plugin_update_cache');
if (false === $remote_data) {
$remote = wp_remote_get(MY_PLUGIN_UPDATE_URL, [
'timeout' => 10,
'headers' => ['Accept' => 'application/json']
]);
if (is_wp_error($remote) || 200 !== wp_remote_retrieve_response_code($remote)) {
return $res;
}
$remote_data = json_decode(wp_remote_retrieve_body($remote));
if (!$remote_data) {
return $res;
}
}
$res = new stdClass();
$res->name = $remote_data->name;
$res->slug = $remote_data->slug;
$res->version = $remote_data->version;
$res->tested = $remote_data->tested;
$res->requires = $remote_data->requires;
$res->requires_php = $remote_data->requires_php;
$res->author = $remote_data->author;
$res->homepage = $remote_data->homepage;
$res->download_link = $remote_data->download_url;
$res->sections = (array)$remote_data->sections;
if (isset($remote_data->banners)) {
$res->banners = (array)$remote_data->banners;
}
if (isset($remote_data->icons)) {
$res->icons = (array)$remote_data->icons;
}
if (isset($remote_data->last_updated)) {
$res->last_updated = $remote_data->last_updated;
}
return $res;
}
/**
* Clear cache when plugin is updated
*/
add_action('upgrader_process_complete', 'my_plugin_clear_update_cache', 10, 2);
function my_plugin_clear_update_cache($upgrader_object, $options) {
if ('update' === $options['action'] && 'plugin' === $options['type']) {
delete_transient('my_plugin_update_cache');
}
}With License Validation
// Add license key field in settings
function my_plugin_check_for_updates($transient) {
$license_key = get_option('my_plugin_license_key');
if (empty($license_key)) {
return $transient; // No license = no updates
}
$remote = wp_remote_post(MY_PLUGIN_UPDATE_URL, [
'body' => [
'license' => $license_key,
'domain' => home_url()
]
]);
// Verify license is valid before offering update
$remote_data = json_decode(wp_remote_retrieve_body($remote));
if (!isset($remote_data->license_valid) || !$remote_data->license_valid) {
return $transient; // Invalid license = no updates
}
// Proceed with update check...
}When to Use
- ✅ Need full control over update logic
- ✅ Want to integrate license verification
- ✅ Building commercial plugin with custom licensing
- ✅ Need analytics/tracking on updates
- ✅ Want to host on own infrastructure
---
Commercial Solutions
Freemius
Website: https://freemius.com Pricing: Free (15% revenue share) or $99-599/year
Features
- ✅ Complete platform: Licensing, payments, updates, analytics
- ✅ WordPress SDK: Drop-in library
- ✅ Automatic updates with license integration
- ✅ In-dashboard checkout: ~12% conversion boost
- ✅ Secure repository: Amazon S3-backed
- ✅ Staged rollouts: Beta testing, gradual releases
Implementation
// Include Freemius SDK
require_once dirname(__FILE__) . '/freemius/start.php';
$my_plugin_fs = fs_dynamic_init([
'id' => '123',
'slug' => 'my-plugin',
'public_key' => 'pk_xxx',
'is_premium' => true,
'has_paid_plans' => true,
'menu' => [
'slug' => 'my-plugin',
],
]);Updates are fully automatic - Freemius handles everything.
When to Use
- ✅ Selling premium plugins commercially
- ✅ Want all-in-one solution (licensing + updates + payments)
- ✅ Need subscription management
- ✅ Okay with revenue share
---
Easy Digital Downloads + Software Licensing
Website: https://easydigitaldownloads.com Pricing: $328/year (Recurring Payments + Software Licensing extensions)
Features
- ✅ Self-hosted: Run on your own WordPress site
- ✅ Full control: Own the data
- ✅ No revenue share: Fixed annual cost
- ✅ Mature ecosystem: 10+ years
Limitations
- ❌ More setup: Requires custom development for update integration
- ❌ No in-dashboard checkout
- ❌ Extension costs add up
When to Use
- ✅ Already using WordPress for sales
- ✅ Want complete control over infrastructure
- ✅ Have development resources
- ❌ Don't want to build update logic (use Freemius instead)
---
Security Best Practices
1. Always Use HTTPS
// ✅ Good
$remote = wp_remote_get('https://example.com/updates.json', [
'sslverify' => true // Explicitly verify SSL
]);
// ❌ Bad
$remote = wp_remote_get('http://example.com/updates.json');Why: HTTPS prevents man-in-the-middle attacks.
2. Implement Token Authentication
For private repos:
$updateChecker->setAuthentication(defined('MY_PLUGIN_TOKEN') ? MY_PLUGIN_TOKEN : '');For custom servers:
$remote = wp_remote_get($update_url, [
'headers' => [
'Authorization' => 'Bearer ' . get_option('my_plugin_license_key')
]
]);Never hardcode tokens - use constants or encrypted options.
3. Validate License Keys
function my_plugin_check_for_updates($transient) {
$license = get_option('my_plugin_license');
// Validate license before offering updates
$validation = wp_remote_post('https://example.com/api/validate', [
'body' => [
'license' => $license,
'domain' => home_url()
]
]);
$license_data = json_decode(wp_remote_retrieve_body($validation));
if (!$license_data->valid) {
return $transient; // No updates for invalid licenses
}
// Proceed...
}4. Use Checksums
Server (info.json):
{
"download_url": "https://example.com/plugin.zip",
"checksum": "sha256:abc123def456...",
"checksum_algorithm": "sha256"
}Plugin:
add_filter('upgrader_pre_install', 'my_plugin_verify_checksum', 10, 2);
function my_plugin_verify_checksum($true, $hook_extra) {
$package = $hook_extra['package'];
// Get expected checksum
$expected = get_transient('my_plugin_expected_checksum');
if (!$expected) {
return $true; // Allow if no checksum available
}
// Download and verify
$downloaded = download_url($package);
if (is_wp_error($downloaded)) {
return $downloaded;
}
$actual = hash_file('sha256', $downloaded);
if (!hash_equals($expected, $actual)) {
@unlink($downloaded);
return new WP_Error('checksum_mismatch', 'Update file corrupted or tampered');
}
return $true;
}5. Implement Package Signing (Advanced)
Server-side (sign ZIP):
$zip_contents = file_get_contents('plugin.zip');
$private_key = openssl_pkey_get_private(file_get_contents('private.pem'));
openssl_sign($zip_contents, $signature, $private_key, OPENSSL_ALGO_SHA256);
$info = [
'download_url' => 'https://example.com/plugin.zip',
'signature' => base64_encode($signature)
];
file_put_contents('info.json', json_encode($info));Plugin-side (verify):
$remote_data = json_decode(wp_remote_retrieve_body($remote));
$zip = file_get_contents($remote_data->download_url);
$signature = base64_decode($remote_data->signature);
// Embed public key in plugin
$public_key = <<<EOD
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
EOD;
$public_key = openssl_pkey_get_public($public_key);
$verified = openssl_verify($zip, $signature, $public_key, OPENSSL_ALGO_SHA256);
if ($verified !== 1) {
return new WP_Error('signature_invalid', 'Update signature verification failed');
}6. Rate Limiting
add_filter('site_transient_update_plugins', 'my_plugin_check_updates_rate_limit');
function my_plugin_check_updates_rate_limit($transient) {
$last_check = get_transient('my_plugin_last_check');
if ($last_check && (time() - $last_check) < HOUR_IN_SECONDS) {
return $transient; // Don't check more than once per hour
}
set_transient('my_plugin_last_check', time(), HOUR_IN_SECONDS);
// Proceed with check...
}7. Error Logging
function my_plugin_check_for_updates($transient) {
$remote = wp_remote_get($update_url);
if (is_wp_error($remote)) {
error_log(sprintf(
'[My Plugin] Update check failed: %s',
$remote->get_error_message()
));
return $transient;
}
// Continue...
}8. Secure Token Storage
// ❌ Bad: Hardcoded
$token = 'ghp_abc123';
// ✅ Good: WordPress constant (wp-config.php)
define('MY_PLUGIN_GITHUB_TOKEN', 'ghp_xxx');
$token = MY_PLUGIN_GITHUB_TOKEN;
// ✅ Better: Encrypted option
function my_plugin_get_token() {
$encrypted = get_option('my_plugin_token_encrypted');
return openssl_decrypt($encrypted, 'AES-256-CBC', wp_salt('auth'));
}---
Comparison Matrix
| Feature | Plugin Update Checker | Git Updater | Custom Server | Freemius |
|---|---|---|---|---|
| Setup Complexity | ⭐⭐⭐⭐⭐ Low | ⭐⭐⭐⭐ Low | ⭐⭐ High | ⭐⭐⭐⭐⭐ Low |
| Cost | Free | Free | Hosting costs | 15% or $99-599/yr |
| GitHub Support | ✅ Yes | ✅ Yes | ✅ (DIY) | ❌ No |
| Private Repos | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| License Management | ❌ DIY | ❌ No | ✅ DIY | ✅ Built-in |
| Package Signing | ❌ No | ❌ No | ✅ DIY | ✅ Built-in |
| HTTPS Required | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Dependencies | Library (bundled) | Plugin (separate) | None | SDK (bundled) |
| Control Level | Medium | Low | High | Medium |
| Maintenance | Low | Low | High | None |
| Scalability | High (GitHub CDN) | High | Varies | High (S3) |
| In-Dashboard Checkout | ❌ No | ❌ No | ✅ DIY | ✅ Yes |
| Analytics | ❌ No | ❌ No | ✅ DIY | ✅ Built-in |
| Best For | Open source, freemium | Multi-platform | Custom licensing | Commercial SaaS |
---
Common Pitfalls
1. Incorrect ZIP Structure
❌ Wrong:
plugin.zip
├── plugin.php
├── readme.txt
└── assets/✅ Correct:
plugin.zip
└── my-plugin/ ← Plugin folder MUST be inside
├── plugin.php
├── readme.txt
└── assets/Fix: Always include plugin folder in ZIP.
2. Missing HTTPS
// ❌ Bad
$url = 'http://example.com/updates.json';
// ✅ Good
$url = 'https://example.com/updates.json';Fix: Always use HTTPS for update URLs.
3. Not Caching Requests
// ❌ Bad: Checks every page load
$remote = wp_remote_get($update_url);
// ✅ Good: Cache for 12 hours
$cached = get_transient('my_plugin_update_data');
if (false === $cached) {
$remote = wp_remote_get($update_url);
$cached = wp_remote_retrieve_body($remote);
set_transient('my_plugin_update_data', $cached, 12 * HOUR_IN_SECONDS);
}Fix: Always cache remote requests.
4. Hardcoded Tokens
// ❌ Bad
$updateChecker->setAuthentication('ghp_abc123');
// ✅ Good
if (defined('MY_PLUGIN_TOKEN')) {
$updateChecker->setAuthentication(MY_PLUGIN_TOKEN);
}Fix: Use constants or encrypted options.
5. No Error Handling
// ❌ Bad
$remote = wp_remote_get($url);
$data = json_decode(wp_remote_retrieve_body($remote));
// ✅ Good
$remote = wp_remote_get($url);
if (is_wp_error($remote)) {
error_log('Update check failed: ' . $remote->get_error_message());
return $transient;
}
if (200 !== wp_remote_retrieve_response_code($remote)) {
error_log('Update check returned: ' . wp_remote_retrieve_response_code($remote));
return $transient;
}
$data = json_decode(wp_remote_retrieve_body($remote));
if (!$data || !isset($data->version)) {
error_log('Invalid update data received');
return $transient;
}Fix: Always validate responses.
6. Version Comparison Issues
// ❌ Bad: String comparison
if (MY_PLUGIN_VERSION < $remote_data->version) {
// Fails: "1.10.0" < "1.2.0" = false (string comparison)
}
// ✅ Good: Semantic version comparison
if (version_compare(MY_PLUGIN_VERSION, $remote_data->version, '<')) {
// Correctly: "1.10.0" < "1.2.0" = false (semantic comparison)
}Fix: Use version_compare().
7. Forgetting to Flush Cache
// After updating plugin, clear cached update data
add_action('upgrader_process_complete', 'my_plugin_clear_cache');
function my_plugin_clear_cache() {
delete_transient('my_plugin_update_cache');
}Fix: Clear transients after updates.
---
Resources
Official Documentation
- Plugin Update Checker: https://github.com/YahnisElsts/plugin-update-checker
- Git Updater: https://github.com/afragen/git-updater/wiki
- WordPress Plugin API: https://developer.wordpress.org/plugins/
- Transients API: https://developer.wordpress.org/apis/transients/
Tutorials
- Self-Hosted Updates: https://rudrastyh.com/wordpress/self-hosted-plugin-update.html
- GitHub Updates: https://code.tutsplus.com/tutorials/distributing-your-plugins-in-github-with-automatic-updates--wp-34817
- Serverless Update Server: https://macarthur.me/posts/serverless-wordpress-plugin-update-server/
Security
- WordPress Security: https://developer.wordpress.org/plugins/wordpress-org/plugin-security/
- Checksum Verification: https://developer.wordpress.org/cli/commands/plugin/verify-checksums/
- Package Signing Proposal: https://core.trac.wordpress.org/ticket/39309
Commercial Platforms
- Freemius: https://freemius.com
- EDD Software Licensing: https://easydigitaldownloads.com/downloads/software-licensing/
---
Recommended: For most developers, use Plugin Update Checker with GitHub. It provides the best balance of simplicity, features, and security.
Next Steps: 1. Choose your update strategy 2. See examples/github-updater.php for complete working example 3. Implement in your plugin 4. Test thoroughly before releasing
---
Last Updated: 2025-11-06 Version: 1.0.0
WordPress Plugin Security Checklist
Complete security audit checklist for WordPress plugins. Use this when reviewing code for security vulnerabilities.
---
1. File Access Protection
ABSPATH Check
Required in EVERY PHP file:
if ( ! defined( 'ABSPATH' ) ) {
exit;
}Why: Prevents direct file access via URL Vulnerability: Remote code execution, information disclosure Source: WordPress Plugin Handbook
✅ Check:
- [ ] All
.phpfiles have ABSPATH check - [ ] Check is at the top of the file (line 2-4)
- [ ] Uses
exitnotdie(WordPress standard)
---
2. Sanitization (Input Validation)
Always Sanitize User Input
Functions to use:
| Input Type | Sanitization Function |
|---|---|
| Text field | sanitize_text_field() |
| Textarea | sanitize_textarea_field() |
sanitize_email() | |
| URL | esc_url_raw() |
| File name | sanitize_file_name() |
| HTML content | wp_kses_post() or wp_kses() |
| Integer | absint() or intval() |
| Float | floatval() |
| Key/Slug | sanitize_key() |
| Title | sanitize_title() |
Example:
// ❌ WRONG - No sanitization
$name = $_POST['name'];
// ✅ CORRECT - Sanitized
$name = sanitize_text_field( $_POST['name'] );✅ Check:
- [ ] All
$_POSTvalues are sanitized - [ ] All
$_GETvalues are sanitized - [ ] All
$_REQUESTvalues are sanitized - [ ] All
$_COOKIEvalues are sanitized - [ ] Correct sanitization function for data type
---
3. Escaping (Output Protection)
Always Escape Output
Functions to use:
| Output Context | Escaping Function |
|---|---|
| HTML content | esc_html() |
| HTML attribute | esc_attr() |
| URL | esc_url() |
| JavaScript | esc_js() |
| Textarea | esc_textarea() |
| HTML blocks | wp_kses_post() |
| Translation | esc_html__(), esc_html_e(), esc_attr__(), esc_attr_e() |
Example:
// ❌ WRONG - No escaping
echo $user_input;
echo '<a href="' . $url . '">Link</a>';
// ✅ CORRECT - Escaped
echo esc_html( $user_input );
echo '<a href="' . esc_url( $url ) . '">Link</a>';✅ Check:
- [ ] All variables in HTML are escaped
- [ ] All variables in attributes are escaped
- [ ] All URLs are escaped
- [ ] Correct escaping function for context
---
4. Nonces (CSRF Protection)
Use Nonces for All Forms and AJAX
Form example:
// Add nonce to form
wp_nonce_field( 'my_action', 'my_nonce_field' );
// Verify nonce when processing
if ( ! wp_verify_nonce( $_POST['my_nonce_field'], 'my_action' ) ) {
wp_die( 'Security check failed' );
}AJAX example:
// Create nonce
wp_create_nonce( 'my_ajax_nonce' );
// Verify in AJAX handler
check_ajax_referer( 'my_ajax_nonce', 'nonce' );URL example:
// Add nonce to URL
$url = wp_nonce_url( admin_url( 'admin-post.php?action=my_action' ), 'my_action' );
// Verify nonce
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'my_action' ) ) {
wp_die( 'Security check failed' );
}✅ Check:
- [ ] All forms have nonce fields
- [ ] All form handlers verify nonces
- [ ] All AJAX handlers verify nonces
- [ ] All admin action URLs have nonces
- [ ] Nonce actions are unique and descriptive
---
5. Capability Checks (Authorization)
Always Check User Permissions
Never use `is_admin()` - it only checks if you're on an admin page, not user permissions!
Correct:
// ❌ WRONG - Only checks admin area
if ( is_admin() ) {
// Anyone can access this!
}
// ✅ CORRECT - Checks user capability
if ( current_user_can( 'manage_options' ) ) {
// Only admins can access
}Common capabilities:
| Capability | Who Has It |
|---|---|
manage_options | Administrator |
edit_posts | Editor, Author, Contributor |
publish_posts | Editor, Author |
edit_published_posts | Editor, Author |
delete_posts | Editor, Author |
upload_files | Editor, Author |
read | All logged-in users |
✅ Check:
- [ ] All admin pages check capabilities
- [ ] All AJAX handlers check capabilities
- [ ] All REST endpoints have permission callbacks
- [ ] All form handlers check capabilities
- [ ] Never rely on
is_admin()alone
---
6. SQL Injection Prevention
Always Use Prepared Statements
Use `$wpdb->prepare()`:
global $wpdb;
// ❌ WRONG - SQL injection vulnerability
$results = $wpdb->get_results( "SELECT * FROM table WHERE id = {$id}" );
// ✅ CORRECT - Prepared statement
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM table WHERE id = %d",
$id
) );Placeholders:
| Type | Placeholder |
|---|---|
| Integer | %d |
| Float | %f |
| String | %s |
✅ Check:
- [ ] All
$wpdbqueries useprepare() - [ ] Correct placeholder for data type
- [ ] Never concatenate variables into SQL
- [ ] User input is sanitized before
prepare()
---
7. Unique Prefixing
Prevent Naming Conflicts
Use 4-5 character prefix for everything:
// Functions
function myplug_init() {}
// Classes
class MyPlug_Admin {}
// Constants
define( 'MYPLUG_VERSION', '1.0.0' );
// Options
get_option( 'myplug_settings' );
// Meta keys
update_post_meta( $id, '_myplug_data', $value );
// AJAX actions
add_action( 'wp_ajax_myplug_action', 'myplug_ajax_handler' );
// REST routes
register_rest_route( 'myplug/v1', '/endpoint', $args );✅ Check:
- [ ] All functions have unique prefix
- [ ] All classes have unique prefix
- [ ] All constants have unique prefix
- [ ] All database options have unique prefix
- [ ] All meta keys have unique prefix (start with
_for hidden) - [ ] All AJAX actions have unique prefix
- [ ] All REST namespaces have unique prefix
---
8. Asset Loading
Load Assets Conditionally
// ❌ WRONG - Loads everywhere
function bad_enqueue() {
wp_enqueue_script( 'my-script', $url );
}
add_action( 'wp_enqueue_scripts', 'bad_enqueue' );
// ✅ CORRECT - Loads only where needed
function good_enqueue() {
if ( is_singular( 'book' ) ) {
wp_enqueue_script( 'my-script', $url );
}
}
add_action( 'wp_enqueue_scripts', 'good_enqueue' );✅ Check:
- [ ] Scripts load only on needed pages
- [ ] Styles load only on needed pages
- [ ] Admin assets use
admin_enqueue_scriptshook - [ ] Admin assets check
$hookparameter - [ ] Dependencies are declared (
array( 'jquery' )) - [ ] Versions are set (for cache busting)
---
9. Data Validation
Validate Before Saving
// Example: Validate select field
$allowed_values = array( 'option1', 'option2', 'option3' );
$value = sanitize_text_field( $_POST['select_field'] );
if ( ! in_array( $value, $allowed_values, true ) ) {
// Invalid value - reject or use default
$value = 'option1';
}✅ Check:
- [ ] Select/radio values validated against allowed values
- [ ] Number fields validated for min/max range
- [ ] Email fields validated with
is_email() - [ ] URLs validated with
esc_url_raw() - [ ] File uploads validated for type and size
---
10. File Upload Security
Validate File Uploads
// Check file type
$allowed_types = array( 'image/jpeg', 'image/png' );
if ( ! in_array( $_FILES['file']['type'], $allowed_types, true ) ) {
wp_die( 'Invalid file type' );
}
// Check file size
$max_size = 5 * 1024 * 1024; // 5MB
if ( $_FILES['file']['size'] > $max_size ) {
wp_die( 'File too large' );
}
// Use WordPress upload handler
$file = $_FILES['file'];
$upload = wp_handle_upload( $file, array( 'test_form' => false ) );✅ Check:
- [ ] File type is validated
- [ ] File size is validated
- [ ] Uses
wp_handle_upload()ormedia_handle_upload() - [ ] File names are sanitized
- [ ] User has
upload_filescapability
---
11. Direct Object Reference
Check Ownership Before Actions
// ❌ WRONG - No ownership check
$post_id = absint( $_POST['post_id'] );
wp_delete_post( $post_id );
// ✅ CORRECT - Check ownership
$post_id = absint( $_POST['post_id'] );
$post = get_post( $post_id );
if ( ! $post || $post->post_author != get_current_user_id() ) {
wp_die( 'Permission denied' );
}
wp_delete_post( $post_id );✅ Check:
- [ ] Delete/edit actions verify ownership
- [ ] Or check appropriate capability
- [ ] REST endpoints verify ownership in permission callback
---
12. REST API Security
Secure REST Endpoints
register_rest_route( 'myplugin/v1', '/items', array(
'methods' => 'POST',
'callback' => 'my_callback',
// ✅ REQUIRED: Permission callback
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
// ✅ REQUIRED: Argument validation
'args' => array(
'title' => array(
'required' => true,
'validate_callback' => function( $param ) {
return ! empty( $param );
},
'sanitize_callback' => 'sanitize_text_field',
),
),
) );✅ Check:
- [ ] All endpoints have
permission_callback - [ ] Never use
'permission_callback' => '__return_true'for write operations - [ ] All parameters have validation
- [ ] All parameters have sanitization
- [ ] Return proper HTTP status codes (200, 400, 401, 404, 500)
---
13. Internationalization Security
Escape Translated Strings
// ❌ WRONG - Vulnerable to XSS
echo __( 'Hello', 'my-plugin' );
// ✅ CORRECT - Escaped
echo esc_html__( 'Hello', 'my-plugin' );
echo esc_html_e( 'Hello', 'my-plugin' );
echo esc_attr__( 'Hello', 'my-plugin' );✅ Check:
- [ ] Use
esc_html__()instead of__() - [ ] Use
esc_html_e()instead of_e() - [ ] Use
esc_attr__()for attributes - [ ] Never output
__()directly
---
14. Data Cleanup
Remove Data on Uninstall (Not Deactivation)
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete options
delete_option( 'myplug_settings' );
// Delete transients
delete_transient( 'myplug_cache' );
// Delete posts (optional - user data loss!)
// Only if absolutely necessary✅ Check:
- [ ] Options deleted in
uninstall.php - [ ] Transients deleted in
uninstall.php - [ ] Never delete data in deactivation hook
- [ ] Consider asking user before deleting post data
---
15. Common Vulnerabilities to Avoid
XSS (Cross-Site Scripting)
- [ ] Never
echouser input without escaping - [ ] Never use
innerHTMLwith user data - [ ] Always escape in HTML context
SQL Injection
- [ ] Never concatenate variables into SQL
- [ ] Always use
$wpdb->prepare() - [ ] Sanitize before
prepare()
CSRF (Cross-Site Request Forgery)
- [ ] All forms have nonces
- [ ] All AJAX has nonces
- [ ] All admin actions have nonces
Authorization Bypass
- [ ] Never use
is_admin()alone - [ ] Always check capabilities
- [ ] Verify ownership for user-specific data
Path Traversal
- [ ] Validate file paths
- [ ] Use
realpath()to resolve paths - [ ] Never allow
../in file operations
---
Quick Security Scan
Run this checklist on every file:
1. [ ] ABSPATH check at top 2. [ ] Unique prefix on all names 3. [ ] All $_POST/$_GET sanitized 4. [ ] All output escaped 5. [ ] All forms/AJAX have nonces 6. [ ] All actions check capabilities 7. [ ] All $wpdb queries use prepare() 8. [ ] Assets load conditionally 9. [ ] No direct file access 10. [ ] No hardcoded credentials
---
Resources
- WordPress Security Whitepaper: https://wordpress.org/about/security/
- Plugin Security: https://developer.wordpress.org/apis/security/
- Patchstack Database: https://patchstack.com/database/
- Wordfence: https://www.wordfence.com/blog/
- WPScan: https://wpscan.com/
---
Last Updated: 2025-11-06 Status: Production Ready
#!/bin/bash
# [TODO: Script Name]
# [TODO: Brief description of what this script does]
# Example script structure - delete if not needed
set -e # Exit on error
# [TODO: Add your script logic here]
echo "Example script - replace or delete this file"
# Usage:
# ./scripts/example-script.sh [args]
#!/bin/bash
# WordPress Plugin Scaffolding Script
# Creates a new WordPress plugin from templates
set -e
echo "======================================"
echo "WordPress Plugin Scaffolding Tool"
echo "======================================"
echo ""
# Check if we're in the right directory
if [ ! -d "../../templates" ]; then
echo "Error: This script must be run from the skills/wordpress-plugin-core/scripts/ directory"
exit 1
fi
# Get plugin information
read -p "Plugin Name (e.g., My Awesome Plugin): " PLUGIN_NAME
read -p "Plugin Slug (e.g., my-awesome-plugin): " PLUGIN_SLUG
read -p "Plugin Prefix (4-5 chars, e.g., myap_): " PLUGIN_PREFIX
read -p "Plugin Author: " PLUGIN_AUTHOR
read -p "Plugin URI: " PLUGIN_URI
read -p "Author URI: " AUTHOR_URI
read -p "Description: " PLUGIN_DESC
# Choose architecture
echo ""
echo "Select plugin architecture:"
echo "1) Simple (functional programming)"
echo "2) OOP (object-oriented, singleton)"
echo "3) PSR-4 (modern, namespaced with Composer)"
read -p "Choice (1-3): " ARCH_CHOICE
# Set template directory
case $ARCH_CHOICE in
1)
TEMPLATE_DIR="../../templates/plugin-simple"
ARCH_NAME="simple"
;;
2)
TEMPLATE_DIR="../../templates/plugin-oop"
ARCH_NAME="oop"
;;
3)
TEMPLATE_DIR="../../templates/plugin-psr4"
ARCH_NAME="psr4"
;;
*)
echo "Invalid choice"
exit 1
;;
esac
# Set destination directory
DEST_DIR="$HOME/wp-content/plugins/$PLUGIN_SLUG"
# Check if destination exists
if [ -d "$DEST_DIR" ]; then
echo "Error: Plugin directory already exists: $DEST_DIR"
exit 1
fi
echo ""
echo "Creating plugin from $ARCH_NAME template..."
# Copy template
cp -r "$TEMPLATE_DIR" "$DEST_DIR"
# Function to replace placeholders in a file
replace_in_file() {
local file="$1"
# Skip vendor directory if it exists
if [[ "$file" == *"/vendor/"* ]]; then
return
fi
# Only process text files
if file "$file" | grep -q text; then
sed -i "s/My Simple Plugin/$PLUGIN_NAME/g" "$file"
sed -i "s/My OOP Plugin/$PLUGIN_NAME/g" "$file"
sed -i "s/My PSR-4 Plugin/$PLUGIN_NAME/g" "$file"
sed -i "s/my-simple-plugin/$PLUGIN_SLUG/g" "$file"
sed -i "s/my-oop-plugin/$PLUGIN_SLUG/g" "$file"
sed -i "s/my-psr4-plugin/$PLUGIN_SLUG/g" "$file"
sed -i "s/mysp_/${PLUGIN_PREFIX}/g" "$file"
sed -i "s/MYSP_/${PLUGIN_PREFIX^^}/g" "$file"
sed -i "s/myop_/${PLUGIN_PREFIX}/g" "$file"
sed -i "s/MYOP_/${PLUGIN_PREFIX^^}/g" "$file"
sed -i "s/mypp_/${PLUGIN_PREFIX}/g" "$file"
sed -i "s/MYPP_/${PLUGIN_PREFIX^^}/g" "$file"
sed -i "s/MyPSR4Plugin/${PLUGIN_PREFIX^}Plugin/g" "$file"
sed -i "s/My_OOP_Plugin/${PLUGIN_PREFIX^}Plugin/g" "$file"
sed -i "s/Your Name/$PLUGIN_AUTHOR/g" "$file"
sed -i "s|https://example.com/my-simple-plugin/|$PLUGIN_URI|g" "$file"
sed -i "s|https://example.com/my-oop-plugin/|$PLUGIN_URI|g" "$file"
sed -i "s|https://example.com/my-psr4-plugin/|$PLUGIN_URI|g" "$file"
sed -i "s|https://example.com/|$AUTHOR_URI|g" "$file"
sed -i "s/A simple WordPress plugin demonstrating functional programming pattern with security best practices./$PLUGIN_DESC/g" "$file"
sed -i "s/An object-oriented WordPress plugin using singleton pattern with security best practices./$PLUGIN_DESC/g" "$file"
sed -i "s/A modern WordPress plugin using PSR-4 autoloading with Composer and namespaces./$PLUGIN_DESC/g" "$file"
fi
}
# Replace placeholders in all files
echo "Replacing placeholders..."
find "$DEST_DIR" -type f | while read -r file; do
replace_in_file "$file"
done
# Rename main plugin file
cd "$DEST_DIR"
if [ "$ARCH_NAME" = "simple" ]; then
mv my-simple-plugin.php "$PLUGIN_SLUG.php"
elif [ "$ARCH_NAME" = "oop" ]; then
mv my-oop-plugin.php "$PLUGIN_SLUG.php"
elif [ "$ARCH_NAME" = "psr4" ]; then
mv my-psr4-plugin.php "$PLUGIN_SLUG.php"
fi
# Create asset directories
mkdir -p assets/css assets/js
# For PSR-4, run composer install if composer is available
if [ "$ARCH_NAME" = "psr4" ] && command -v composer &> /dev/null; then
echo "Running composer install..."
composer install
fi
echo ""
echo "✅ Plugin created successfully!"
echo ""
echo "Location: $DEST_DIR"
echo ""
echo "Next steps:"
echo "1. Activate plugin in WordPress admin"
echo "2. Create assets/css/ and assets/js/ files as needed"
if [ "$ARCH_NAME" = "psr4" ]; then
echo "3. Run 'composer install' if not already done"
echo "4. Add new classes to src/ directory"
fi
echo ""
echo "Security reminder:"
echo "- All files have ABSPATH checks ✅"
echo "- Unique prefix ($PLUGIN_PREFIX) applied ✅"
echo "- Remember to:"
echo " - Sanitize all input"
echo " - Escape all output"
echo " - Use nonces for forms/AJAX"
echo " - Check capabilities"
echo " - Use prepared statements for database"
echo ""
<?php
/**
* Example: AJAX handlers
*
* This example shows how to:
* - Register AJAX handlers for logged-in and non-logged-in users
* - Verify nonces for CSRF protection
* - Check user capabilities
* - Sanitize input and return JSON responses
* - Enqueue scripts with localized data
*
* @package YourPlugin
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Enqueue AJAX script
*/
function yourprefix_enqueue_ajax_script() {
wp_enqueue_script(
'yourprefix-ajax',
plugins_url( 'js/ajax-example.js', __FILE__ ),
array( 'jquery' ),
'1.0.0',
true
);
// Localize script with AJAX URL and nonce
wp_localize_script(
'yourprefix-ajax',
'yourprefixAjax',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'yourprefix_ajax_nonce' ),
)
);
}
add_action( 'wp_enqueue_scripts', 'yourprefix_enqueue_ajax_script' );
/**
* AJAX handler for logged-in users
*/
function yourprefix_ajax_save_data() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Check user capability
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array(
'message' => __( 'Permission denied', 'your-plugin' ),
) );
}
// Get and sanitize input
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '';
$age = isset( $_POST['age'] ) ? absint( $_POST['age'] ) : 0;
// Validate input
if ( empty( $name ) ) {
wp_send_json_error( array(
'message' => __( 'Name is required', 'your-plugin' ),
) );
}
if ( ! is_email( $email ) ) {
wp_send_json_error( array(
'message' => __( 'Invalid email address', 'your-plugin' ),
) );
}
// Process data (example: save to database)
$result = yourprefix_save_user_data( $name, $email, $age );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array(
'message' => $result->get_error_message(),
) );
}
// Return success response
wp_send_json_success( array(
'message' => __( 'Data saved successfully', 'your-plugin' ),
'data' => array(
'name' => $name,
'email' => $email,
'age' => $age,
),
) );
}
add_action( 'wp_ajax_yourprefix_save_data', 'yourprefix_ajax_save_data' );
/**
* AJAX handler for non-logged-in users
*/
function yourprefix_ajax_public_action() {
// Verify nonce (still required for public AJAX)
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Get and sanitize input
$query = isset( $_POST['query'] ) ? sanitize_text_field( $_POST['query'] ) : '';
if ( empty( $query ) ) {
wp_send_json_error( array(
'message' => __( 'Query is required', 'your-plugin' ),
) );
}
// Process query (example: search posts)
$results = yourprefix_search_posts( $query );
wp_send_json_success( array(
'message' => __( 'Search completed', 'your-plugin' ),
'results' => $results,
) );
}
add_action( 'wp_ajax_yourprefix_public_action', 'yourprefix_ajax_public_action' );
add_action( 'wp_ajax_nopriv_yourprefix_public_action', 'yourprefix_ajax_public_action' );
/**
* AJAX handler for fetching posts
*/
function yourprefix_ajax_load_posts() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Get parameters
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
$per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;
$category = isset( $_POST['category'] ) ? absint( $_POST['category'] ) : 0;
// Query posts
$args = array(
'post_type' => 'post',
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
);
if ( $category > 0 ) {
$args['cat'] = $category;
}
$query = new WP_Query( $args );
$posts = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$posts[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'excerpt' => get_the_excerpt(),
'url' => get_permalink(),
'date' => get_the_date(),
);
}
wp_reset_postdata();
}
wp_send_json_success( array(
'posts' => $posts,
'total_pages' => $query->max_num_pages,
'found_posts' => $query->found_posts,
) );
}
add_action( 'wp_ajax_yourprefix_load_posts', 'yourprefix_ajax_load_posts' );
add_action( 'wp_ajax_nopriv_yourprefix_load_posts', 'yourprefix_ajax_load_posts' );
/**
* AJAX handler for deleting item
*/
function yourprefix_ajax_delete_item() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Check user capability
if ( ! current_user_can( 'delete_posts' ) ) {
wp_send_json_error( array(
'message' => __( 'Permission denied', 'your-plugin' ),
) );
}
// Get item ID
$item_id = isset( $_POST['item_id'] ) ? absint( $_POST['item_id'] ) : 0;
if ( $item_id === 0 ) {
wp_send_json_error( array(
'message' => __( 'Invalid item ID', 'your-plugin' ),
) );
}
// Check if item exists
$post = get_post( $item_id );
if ( ! $post ) {
wp_send_json_error( array(
'message' => __( 'Item not found', 'your-plugin' ),
) );
}
// Delete item
$result = wp_trash_post( $item_id );
if ( ! $result ) {
wp_send_json_error( array(
'message' => __( 'Failed to delete item', 'your-plugin' ),
) );
}
wp_send_json_success( array(
'message' => __( 'Item deleted successfully', 'your-plugin' ),
'item_id' => $item_id,
) );
}
add_action( 'wp_ajax_yourprefix_delete_item', 'yourprefix_ajax_delete_item' );
/**
* AJAX handler for uploading file
*/
function yourprefix_ajax_upload_file() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Check user capability
if ( ! current_user_can( 'upload_files' ) ) {
wp_send_json_error( array(
'message' => __( 'Permission denied', 'your-plugin' ),
) );
}
// Check if file was uploaded
if ( empty( $_FILES['file'] ) ) {
wp_send_json_error( array(
'message' => __( 'No file uploaded', 'your-plugin' ),
) );
}
// Handle file upload
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$file = $_FILES['file'];
// Validate file type
$allowed_types = array( 'image/jpeg', 'image/png', 'image/gif' );
if ( ! in_array( $file['type'], $allowed_types, true ) ) {
wp_send_json_error( array(
'message' => __( 'Invalid file type. Only JPG, PNG, and GIF are allowed.', 'your-plugin' ),
) );
}
// Upload file
$attachment_id = media_handle_upload( 'file', 0 );
if ( is_wp_error( $attachment_id ) ) {
wp_send_json_error( array(
'message' => $attachment_id->get_error_message(),
) );
}
// Get attachment data
$attachment_url = wp_get_attachment_url( $attachment_id );
wp_send_json_success( array(
'message' => __( 'File uploaded successfully', 'your-plugin' ),
'attachment_id' => $attachment_id,
'attachment_url' => $attachment_url,
) );
}
add_action( 'wp_ajax_yourprefix_upload_file', 'yourprefix_ajax_upload_file' );
/**
* Helper function: Save user data
*
* @param string $name User name.
* @param string $email User email.
* @param int $age User age.
* @return bool|WP_Error
*/
function yourprefix_save_user_data( $name, $email, $age ) {
global $wpdb;
$table_name = $wpdb->prefix . 'yourprefix_users';
$result = $wpdb->insert(
$table_name,
array(
'name' => $name,
'email' => $email,
'age' => $age,
),
array( '%s', '%s', '%d' )
);
if ( false === $result ) {
return new WP_Error( 'db_error', __( 'Database error', 'your-plugin' ) );
}
return true;
}
/**
* Helper function: Search posts
*
* @param string $query Search query.
* @return array
*/
function yourprefix_search_posts( $query ) {
$args = array(
'post_type' => 'post',
's' => $query,
'posts_per_page' => 10,
'post_status' => 'publish',
);
$search_query = new WP_Query( $args );
$results = array();
if ( $search_query->have_posts() ) {
while ( $search_query->have_posts() ) {
$search_query->the_post();
$results[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'url' => get_permalink(),
);
}
wp_reset_postdata();
}
return $results;
}
<?php
/**
* Uninstall script
*
* This file is called when the plugin is uninstalled via WordPress admin.
*/
// Exit if not called by WordPress
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete plugin options
delete_option( 'myop_settings' );
delete_option( 'myop_activated_time' );
// Delete transients
delete_transient( 'myop_cache' );
// For multisite
if ( is_multisite() ) {
global $wpdb;
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
delete_option( 'myop_settings' );
delete_option( 'myop_activated_time' );
delete_transient( 'myop_cache' );
restore_current_blog();
}
}
// Delete custom post type data (optional)
/*
$books = get_posts( array(
'post_type' => 'book',
'posts_per_page' => -1,
'post_status' => 'any',
) );
foreach ( $books as $book ) {
wp_delete_post( $book->ID, true );
}
*/
{
"name": "yourname/my-psr4-plugin",
"description": "A modern WordPress plugin using PSR-4 autoloading",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Your Name",
"email": "your@email.com"
}
],
"require": {
"php": ">=7.4"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7",
"wp-coding-standards/wpcs": "^3.0"
},
"autoload": {
"psr-4": {
"MyPSR4Plugin\\": "src/"
}
},
"scripts": {
"phpcs": "phpcs --standard=WordPress src/",
"phpcbf": "phpcbf --standard=WordPress src/"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}