commit 73ee9697a54c203794bb2aa4ef212f45ca3d8edc Author: Zhongwei Li Date: Sun Nov 30 08:25:50 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..2e90ba6 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "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" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..66eab24 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# wordpress-plugin-core + +Build secure WordPress plugins with core patterns for hooks, database interactions, Settings API, custom post types, REST API, and AJAX. Covers three architecture patterns (Simple, OOP, PSR-4) and the Security Trinity. Use when creating plugins, implementing nonces/sanitization/escaping, working with $wpdb prepared statements, or troubleshooting SQL injection, XSS, CSRF vulnerabilities, or plugin activation errors. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..f8f638e --- /dev/null +++ b/SKILL.md @@ -0,0 +1,1519 @@ +--- +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. +license: MIT +--- + +# 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 +" /> + +// 4. Sanitize Input, Escape Output +$clean = sanitize_text_field( $_POST['input'] ); +echo esc_html( $output ); + +// 5. Prepared Statements for Database +global $wpdb; +$results = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", + $id + ) +); +``` + +--- + +## The 5-Step Security Foundation + +WordPress plugin security has THREE components that must ALL be present: + +### Step 1: Use Unique Prefix for Everything + +**Why**: Prevents naming conflicts with other plugins and WordPress core. + +**Rules**: +- 4-5 characters minimum +- Apply to: functions, classes, constants, options, transients, meta keys, global variables +- Avoid: `wp_`, `__`, `_`, "WordPress" + +```php +// GOOD +function mypl_function_name() {} +class MyPL_Class_Name {} +define( 'MYPL_CONSTANT', 'value' ); +add_option( 'mypl_option', 'value' ); +set_transient( 'mypl_cache', $data, HOUR_IN_SECONDS ); + +// BAD +function function_name() {} // No prefix, will conflict +class Settings {} // Too generic +``` + +### Step 2: Check Capabilities, Not Just Admin Status + +**ERROR**: Using `is_admin()` for permission checks + +```php +// WRONG - Anyone can access admin area URLs +if ( is_admin() ) { + // Delete user data - SECURITY HOLE +} + +// CORRECT - Check user capability +if ( current_user_can( 'manage_options' ) ) { + // Delete user data - Now secure +} +``` + +**Common Capabilities**: +- `manage_options` - Administrator +- `edit_posts` - Editor/Author +- `publish_posts` - Author +- `edit_pages` - Editor +- `read` - Subscriber + +### Step 3: The Security Trinity + +**Input → Processing → Output** each require different functions: + +```php +// SANITIZATION (Input) - Clean user data +$name = sanitize_text_field( $_POST['name'] ); +$email = sanitize_email( $_POST['email'] ); +$url = esc_url_raw( $_POST['url'] ); +$html = wp_kses_post( $_POST['content'] ); // Allow safe HTML +$key = sanitize_key( $_POST['option'] ); +$ids = array_map( 'absint', $_POST['ids'] ); // Array of integers + +// VALIDATION (Logic) - Verify it meets requirements +if ( ! is_email( $email ) ) { + wp_die( 'Invalid email' ); +} + +// ESCAPING (Output) - Make safe for display +echo esc_html( $name ); +echo ''; +echo '
'; +echo ''; +``` + +**Critical Rule**: Sanitize on INPUT, escape on OUTPUT. Never trust user data. + +### Step 4: Nonces (CSRF Protection) + +**What**: One-time tokens that prove requests came from your site. + +**Form Pattern**: + +```php +// Generate nonce in form +
+ + + +
+ +// Verify nonce in handler +if ( ! isset( $_POST['mypl_nonce'] ) || ! wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' ) ) { + wp_die( 'Security check failed' ); +} + +// Now safe to proceed +$data = sanitize_text_field( $_POST['data'] ); +``` + +**AJAX Pattern**: + +```javascript +// JavaScript +jQuery.ajax({ + url: ajaxurl, + data: { + action: 'mypl_ajax_action', + nonce: mypl_ajax_object.nonce, + data: formData + } +}); +``` + +```php +// PHP Handler +function mypl_ajax_handler() { + check_ajax_referer( 'mypl-ajax-nonce', 'nonce' ); + + // Safe to proceed + wp_send_json_success( array( 'message' => 'Success' ) ); +} +add_action( 'wp_ajax_mypl_ajax_action', 'mypl_ajax_handler' ); + +// Localize script with nonce +wp_localize_script( 'mypl-script', 'mypl_ajax_object', array( + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ), +) ); +``` + +### Step 5: Prepared Statements for Database + +**CRITICAL**: Always use `$wpdb->prepare()` for queries with user input. + +```php +global $wpdb; + +// WRONG - SQL Injection vulnerability +$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" ); + +// CORRECT - Prepared statement +$results = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", + $_GET['id'] + ) +); +``` + +**Placeholders**: +- `%s` - String +- `%d` - Integer +- `%f` - Float + +**LIKE Queries** (Special Case): + +```php +$search = '%' . $wpdb->esc_like( $term ) . '%'; +$results = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}posts WHERE post_title LIKE %s", + $search + ) +); +``` + +--- + +## Critical Rules + +### Always Do + +✅ **Use unique prefix** (4-5 chars) for all global code (functions, classes, options, transients) +✅ **Add ABSPATH check** to every PHP file: `if ( ! defined( 'ABSPATH' ) ) exit;` +✅ **Check capabilities** (`current_user_can()`) not just `is_admin()` +✅ **Verify nonces** for all forms and AJAX requests +✅ **Use $wpdb->prepare()** for all database queries with user input +✅ **Sanitize input** with `sanitize_*()` functions before saving +✅ **Escape output** with `esc_*()` functions before displaying +✅ **Flush rewrite rules** on activation when registering custom post types +✅ **Use uninstall.php** for permanent cleanup (not deactivation hook) +✅ **Follow WordPress Coding Standards** (tabs for indentation, Yoda conditions) + +### Never Do + +❌ **Never use extract()** - Creates security vulnerabilities +❌ **Never trust $_POST/$_GET** without sanitization +❌ **Never concatenate user input into SQL** - Always use prepare() +❌ **Never use `is_admin()` alone** for permission checks +❌ **Never output unsanitized data** - Always escape +❌ **Never use generic function/class names** - Always prefix +❌ **Never use short PHP tags** `prepare()` with placeholders + +```php +// 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 + +```php +// VULNERABLE +echo $_POST['name']; +echo '
'; + +// SECURE +echo esc_html( $_POST['name'] ); +echo '
'; +``` + +### 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()` + +```php +// 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 + +```php +// 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 + +```php +// 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 + +```php +// 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 + +```php +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 + +```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 + +```php +// 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 + +```php +// 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()` + +```php +// 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 + +```php +// 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 + +```php +// 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 + +```php +// BAD - Runs on every page load +register_uninstall_hook( __FILE__, 'mypl_uninstall' ); + +// GOOD - Use uninstall.php file (preferred method) +// Create uninstall.php in plugin root +``` + +### Issue #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 + +```php +// 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.php +``` + +### Issue #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 + +```php +// 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 + +```php +// 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 + +```php +add_action( 'plugins_loaded', function() { + if ( ! class_exists( 'WooCommerce' ) ) { + add_action( 'admin_notices', function() { + echo '

My Plugin requires WooCommerce.

'; + } ); + 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 + +```php +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) + +```php +// 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 + +
+

My Plugin Settings

+
+ 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() { + ?> +
+

My Plugin Settings

+
+ =7.4" + } +} +``` + +**my-plugin.php**: +```php +init(); + } + + private function init() { + new Admin(); + new Frontend(); + } +} + +MyPlugin::get_instance(); +``` + +**src/Admin.php**: +```php + +
+

My Plugin Settings

+
+ 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 + +```php +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 + +```php +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' ); + ?> + + + '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 '

Configure your API settings.

'; +} + +function mypl_field_callback() { + $value = get_option( 'mypl_api_key' ); + ?> + + +
+

+
+ +
+
+ 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) + +```php +// 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)**: +```javascript +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 + +```php +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 + +```php +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 functions +- `templates/plugin-oop/` - Object-oriented plugin structure +- `templates/plugin-psr4/` - Modern PSR-4 plugin with Composer +- `templates/examples/meta-box.php` - Meta box implementation +- `templates/examples/settings-page.php` - Settings API page +- `templates/examples/custom-post-type.php` - CPT registration +- `templates/examples/rest-endpoint.php` - REST API endpoint +- `templates/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 scaffolding +- `scripts/check-security.sh` - Security audit for common issues +- `scripts/validate-headers.sh` - Verify plugin headers + +**Example Usage:** +```bash +# 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.php +``` + +### References (references/) + +Detailed documentation that Claude can load when needed: + +- `references/security-checklist.md` - Complete security audit checklist +- `references/hooks-reference.md` - Common WordPress hooks and filters +- `references/sanitization-guide.md` - All sanitization/escaping functions +- `references/wpdb-patterns.md` - Database query patterns +- `references/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) + +```php +// 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 echo +``` + +### WP-CLI Commands + +```php +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) + +```php +// 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 + +```php +add_action( 'admin_init', function() { + // Check for WooCommerce + if ( ! class_exists( 'WooCommerce' ) ) { + deactivate_plugins( plugin_basename( __FILE__ ) ); + + add_action( 'admin_notices', function() { + echo '

My Plugin requires WooCommerce to be installed and active.

'; + } ); + + 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:** + +```php +// 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:** + +```bash +# 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 users +``` + +**Key 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 +```php +// Temporarily add to wp-admin +flush_rewrite_rules(); +// Remove after visiting wp-admin once +``` + +### Problem: 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 diff --git a/assets/example-template.txt b/assets/example-template.txt new file mode 100644 index 0000000..349fec2 --- /dev/null +++ b/assets/example-template.txt @@ -0,0 +1,14 @@ +[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. diff --git a/examples/github-updater.php b/examples/github-updater.php new file mode 100644 index 0000000..ee812f0 --- /dev/null +++ b/examples/github-updater.php @@ -0,0 +1,544 @@ +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() { + ?> +
+

+ + +

+
+ 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 ); + ?> + +

+ +

+ 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": "

1.0.1

  • Bug fixes
" + * }, + * "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 + */ diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..6b5c3d7 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,165 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jezweb/claude-skills:skills/wordpress-plugin-core", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "47cb26670b615d9f163d79dedb2554b786e83976", + "treeHash": "95a5d814726399141a4539dddac85998b9341de7ff3040a2bf6fb2f57dddb2e4", + "generatedAt": "2025-11-28T10:19:06.170140Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "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" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "eda536ea2884a345bde6620ca5b7d97e2ca9df45052be053f4cc85ad538bcbf1" + }, + { + "path": "SKILL.md", + "sha256": "8de473cfda58ad8ff555e4c03ac97c7a3e3d456c723cf7d3bd741d4536e0f8dc" + }, + { + "path": "references/example-reference.md", + "sha256": "77c788d727d05d6479a61d6652b132e43882ffc67c145bb46ba880567d83f7f8" + }, + { + "path": "references/github-auto-updates.md", + "sha256": "22f4f38b832767ad170478a96d6f110770bdcee2ba0128742b371db9e751cc67" + }, + { + "path": "references/common-hooks.md", + "sha256": "531fa58b6c13592b96802b53908141bc51572fd8063e0b12be71952d680dda50" + }, + { + "path": "references/security-checklist.md", + "sha256": "8d07b6cf9eac37444f64e1c49e0320b40c24c3e4e216074705e2ee85b502554e" + }, + { + "path": "examples/github-updater.php", + "sha256": "a840b4cf701629474d367c6a67e99a378819c2cab237c2d793bf158180651efd" + }, + { + "path": "scripts/scaffold-plugin.sh", + "sha256": "fd5445dce2080ed9726f0fbac0a66093e8becc35074608188ffc7dcf81bde9dd" + }, + { + "path": "scripts/example-script.sh", + "sha256": "83d2b09d044811608e17cbd8e66d993b1e9998c7bd3379a42ab81fbdba973e0e" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "94ac4b36545ed36289e02c068778b472c33e5e8aca162d9c13542fcd89c5cbf9" + }, + { + "path": "templates/plugin-simple/my-simple-plugin.php", + "sha256": "96f7b47d7bbf960f2d47ad607e83b89735e9bdf6d3fe8db8dd170bad7290bec1" + }, + { + "path": "templates/plugin-simple/README.md", + "sha256": "b11072e34ae49faf1269f662ab4da43db92ca24db361d85493163a7e51b9c981" + }, + { + "path": "templates/plugin-simple/uninstall.php", + "sha256": "0e16e9186b94c2060ed9615a0900886f819f26ca99e85850870faf21141efd9a" + }, + { + "path": "templates/plugin-psr4/README.md", + "sha256": "7b2c81ea7d93c5b0b87b5d54f0a3ed5ce7689de6989f17de34920d1c8b1b32c2" + }, + { + "path": "templates/plugin-psr4/uninstall.php", + "sha256": "d72eea91563b8b2de09de02e10d90105328f5a7e3828efdabcd4948925370e0b" + }, + { + "path": "templates/plugin-psr4/my-psr4-plugin.php", + "sha256": "3890a0eeb7614e4e1b5f901e852c91b9285864ec5ebe128c71beb4dd2f898c69" + }, + { + "path": "templates/plugin-psr4/composer.json", + "sha256": "4583118c8f2f4ca0cb776963ae5b92cc3d6b625c01a80cd04d00bb107d5de1a5" + }, + { + "path": "templates/plugin-psr4/src/Plugin.php", + "sha256": "e405047854beb02ea039233e666ba2abcd95454af9cc2d85c0d577c31fca089b" + }, + { + "path": "templates/plugin-psr4/src/PostTypes/Book.php", + "sha256": "ed234a6f93dc3e43c60626dc8b99b72a7a2c39aab701e190d5ad1da1ca630d50" + }, + { + "path": "templates/plugin-psr4/src/Frontend/Assets.php", + "sha256": "73d5c4e647f28f4903fcdaee136a3165da0eadc8194501dbffe2f78f1402b9be" + }, + { + "path": "templates/plugin-psr4/src/Admin/Settings.php", + "sha256": "c5f5982f2573aea84b312b8d0b968a8babd20e7b6faba2ecc091d299c0649048" + }, + { + "path": "templates/plugin-psr4/src/API/BookEndpoints.php", + "sha256": "e744a87e54620f3de502a456ba8c5fcc0f906aebd5fa6a7f0d5208a719e96ae8" + }, + { + "path": "templates/plugin-psr4/src/Taxonomies/Genre.php", + "sha256": "e6ffeb08cd92bc14cb5c4ce6d56a041adecbe4b4147d5705bed27f8a7c1ed3f5" + }, + { + "path": "templates/examples/settings-page.php", + "sha256": "03440a1e47bac3a857c3fd5585730f442023696347509b4968a0ffcd907e65ec" + }, + { + "path": "templates/examples/ajax-handler.php", + "sha256": "1900998cf7f540cab56d4c92d8bb44545cac1a5ec943c708a602b5fa44716c4e" + }, + { + "path": "templates/examples/rest-endpoint.php", + "sha256": "89ecd48968608eca21b5506be42e32062d056f5ffc13bb61a0015dbc9c15e961" + }, + { + "path": "templates/examples/meta-box.php", + "sha256": "33fff507b5e28bba95c81550ea699d764ee1f03db80b070e11d275c343ad55c6" + }, + { + "path": "templates/examples/custom-post-type.php", + "sha256": "51b378a2a65fbe7d8565959df4f938ec312fe59a911bfd783872193d76ec0dc0" + }, + { + "path": "templates/plugin-oop/my-oop-plugin.php", + "sha256": "9f2212686070f003c8ab3cd66bc6be4cb36b1b8d3065d933afc54c22d072a86d" + }, + { + "path": "templates/plugin-oop/README.md", + "sha256": "4c7987bcb8055e077d579e4fc7fe9826a3b5e8189d536b47be5e07041a944e52" + }, + { + "path": "templates/plugin-oop/uninstall.php", + "sha256": "3de2318eebd139c9b9bb0ec52f8dba6e2d7fc6e630576310fd54d1d00dc40e70" + }, + { + "path": "templates/plugin-oop/views/admin-settings.php", + "sha256": "d3d6c877497e563df427e9f6da30ebf72c8c3bf0fa8ef78562f1a3b891c03458" + }, + { + "path": "assets/example-template.txt", + "sha256": "3f725c80d70847fd8272bf1400515ba753f12f98f3b294d09e50b54b4c1b024a" + } + ], + "dirSha256": "95a5d814726399141a4539dddac85998b9341de7ff3040a2bf6fb2f57dddb2e4" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/references/common-hooks.md b/references/common-hooks.md new file mode 100644 index 0000000..974871b --- /dev/null +++ b/references/common-hooks.md @@ -0,0 +1,98 @@ +# 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 `` section | Add meta tags, styles | +| `wp_footer` | Before `` | 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. + +```php +// 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 diff --git a/references/example-reference.md b/references/example-reference.md new file mode 100644 index 0000000..1be1b40 --- /dev/null +++ b/references/example-reference.md @@ -0,0 +1,26 @@ +# [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. diff --git a/references/github-auto-updates.md b/references/github-auto-updates.md new file mode 100644 index 0000000..bbb286b --- /dev/null +++ b/references/github-auto-updates.md @@ -0,0 +1,1224 @@ +# 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](#overview) +2. [How WordPress Updates Work](#how-wordpress-updates-work) +3. [Solution 1: Plugin Update Checker (Recommended)](#solution-1-plugin-update-checker-recommended) +4. [Solution 2: Git Updater](#solution-2-git-updater) +5. [Solution 3: Custom Update Server](#solution-3-custom-update-server) +6. [Commercial Solutions](#commercial-solutions) +7. [Security Best Practices](#security-best-practices) +8. [Comparison Matrix](#comparison-matrix) +9. [Common Pitfalls](#common-pitfalls) +10. [Resources](#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 + +```php +// 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.txt` format + +### 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) + +```bash +cd your-plugin/ +git submodule add https://github.com/YahnisElsts/plugin-update-checker.git +``` + +**Option B: Composer** + +```bash +composer require yahnis-elsts/plugin-update-checker +``` + +**Option C: Manual** + +```bash +cd your-plugin/ +git clone https://github.com/YahnisElsts/plugin-update-checker.git +# Or download and extract ZIP +``` + +### Basic Implementation + +```php +setBranch('main'); +``` + +### Private Repository Support + +```php +// 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 + +```bash +# 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**: + +```php +// 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 + +```bash +# Just tag the commit +git tag 1.0.1 +git push origin 1.0.1 +``` + +**No 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 + +```php +// Point to specific branch +$myUpdateChecker->setBranch('stable'); + +// Or beta branch +$myUpdateChecker->setBranch('beta'); +``` + +**Update version in plugin header on that branch**: + +```php +/** + * 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 +

'; + echo 'My Plugin: Update checker library not found. '; + echo 'Automatic updates disabled.'; + echo '

'; + }); + 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**: + +```bash +#!/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**: + +```bash +# .gitattributes +.git export-ignore +.gitignore export-ignore +.gitattributes export-ignore +tests/ export-ignore +node_modules/ export-ignore +src/ export-ignore +.github/ export-ignore +``` + +### readme.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 +Installation
  1. Upload plugin
", + "changelog": "

1.0.1

  • Bug fixes
" + }, + "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 +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 + +```php +// 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 + +```php +// 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 + +```php +// ✅ 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**: + +```php +$updateChecker->setAuthentication(defined('MY_PLUGIN_TOKEN') ? MY_PLUGIN_TOKEN : ''); +``` + +**For custom servers**: + +```php +$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 + +```php +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): + +```json +{ + "download_url": "https://example.com/plugin.zip", + "checksum": "sha256:abc123def456...", + "checksum_algorithm": "sha256" +} +``` + +**Plugin**: + +```php +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): + +```php +$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): + +```php +$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 = <<get_error_message() + )); + return $transient; + } + + // Continue... +} +``` + +### 8. Secure Token Storage + +```php +// ❌ 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 + +```php +// ❌ 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 + +```php +// ❌ 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 + +```php +// ❌ 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 + +```php +// ❌ 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 + +```php +// ❌ 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 + +```php +// 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 diff --git a/references/security-checklist.md b/references/security-checklist.md new file mode 100644 index 0000000..211384c --- /dev/null +++ b/references/security-checklist.md @@ -0,0 +1,527 @@ +# 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**: + +```php +if ( ! defined( 'ABSPATH' ) ) { + exit; +} +``` + +**Why**: Prevents direct file access via URL +**Vulnerability**: Remote code execution, information disclosure +**Source**: [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/plugin-basics/best-practices/#file-organization) + +✅ **Check**: +- [ ] All `.php` files have ABSPATH check +- [ ] Check is at the top of the file (line 2-4) +- [ ] Uses `exit` not `die` (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()` | +| Email | `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**: + +```php +// ❌ WRONG - No sanitization +$name = $_POST['name']; + +// ✅ CORRECT - Sanitized +$name = sanitize_text_field( $_POST['name'] ); +``` + +✅ **Check**: +- [ ] All `$_POST` values are sanitized +- [ ] All `$_GET` values are sanitized +- [ ] All `$_REQUEST` values are sanitized +- [ ] All `$_COOKIE` values 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**: + +```php +// ❌ WRONG - No escaping +echo $user_input; +echo '
Link'; + +// ✅ CORRECT - Escaped +echo esc_html( $user_input ); +echo 'Link'; +``` + +✅ **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**: + +```php +// 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**: + +```php +// Create nonce +wp_create_nonce( 'my_ajax_nonce' ); + +// Verify in AJAX handler +check_ajax_referer( 'my_ajax_nonce', 'nonce' ); +``` + +**URL example**: + +```php +// 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**: + +```php +// ❌ 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()`**: + +```php +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 `$wpdb` queries use `prepare()` +- [ ] 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**: + +```php +// 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 + +```php +// ❌ 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_scripts` hook +- [ ] Admin assets check `$hook` parameter +- [ ] Dependencies are declared (`array( 'jquery' )`) +- [ ] Versions are set (for cache busting) + +--- + +## 9. Data Validation + +### Validate Before Saving + +```php +// 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 + +```php +// 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()` or `media_handle_upload()` +- [ ] File names are sanitized +- [ ] User has `upload_files` capability + +--- + +## 11. Direct Object Reference + +### Check Ownership Before Actions + +```php +// ❌ 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 + +```php +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 + +```php +// ❌ 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) + +```php +// 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 `echo` user input without escaping +- [ ] Never use `innerHTML` with 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 diff --git a/scripts/example-script.sh b/scripts/example-script.sh new file mode 100755 index 0000000..1c0c72e --- /dev/null +++ b/scripts/example-script.sh @@ -0,0 +1,15 @@ +#!/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] diff --git a/scripts/scaffold-plugin.sh b/scripts/scaffold-plugin.sh new file mode 100755 index 0000000..c84c5d8 --- /dev/null +++ b/scripts/scaffold-plugin.sh @@ -0,0 +1,154 @@ +#!/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 "" diff --git a/templates/examples/ajax-handler.php b/templates/examples/ajax-handler.php new file mode 100644 index 0000000..d560c98 --- /dev/null +++ b/templates/examples/ajax-handler.php @@ -0,0 +1,337 @@ + 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; +} diff --git a/templates/examples/custom-post-type.php b/templates/examples/custom-post-type.php new file mode 100644 index 0000000..9f7a200 --- /dev/null +++ b/templates/examples/custom-post-type.php @@ -0,0 +1,274 @@ + _x( 'Books', 'Post type general name', 'your-plugin' ), + 'singular_name' => _x( 'Book', 'Post type singular name', 'your-plugin' ), + 'menu_name' => _x( 'Books', 'Admin Menu text', 'your-plugin' ), + 'name_admin_bar' => _x( 'Book', 'Add New on Toolbar', 'your-plugin' ), + 'add_new' => __( 'Add New', 'your-plugin' ), + 'add_new_item' => __( 'Add New Book', 'your-plugin' ), + 'new_item' => __( 'New Book', 'your-plugin' ), + 'edit_item' => __( 'Edit Book', 'your-plugin' ), + 'view_item' => __( 'View Book', 'your-plugin' ), + 'all_items' => __( 'All Books', 'your-plugin' ), + 'search_items' => __( 'Search Books', 'your-plugin' ), + 'parent_item_colon' => __( 'Parent Books:', 'your-plugin' ), + 'not_found' => __( 'No books found.', 'your-plugin' ), + 'not_found_in_trash' => __( 'No books found in Trash.', 'your-plugin' ), + 'featured_image' => _x( 'Book Cover Image', 'Overrides the "Featured Image" phrase', 'your-plugin' ), + 'set_featured_image' => _x( 'Set cover image', 'Overrides the "Set featured image" phrase', 'your-plugin' ), + 'remove_featured_image' => _x( 'Remove cover image', 'Overrides the "Remove featured image" phrase', 'your-plugin' ), + 'use_featured_image' => _x( 'Use as cover image', 'Overrides the "Use as featured image" phrase', 'your-plugin' ), + 'archives' => _x( 'Book archives', 'The post type archive label', 'your-plugin' ), + 'insert_into_item' => _x( 'Insert into book', 'Overrides the "Insert into post"/"Insert into page" phrase', 'your-plugin' ), + 'uploaded_to_this_item' => _x( 'Uploaded to this book', 'Overrides the "Uploaded to this post"/"Uploaded to this page" phrase', 'your-plugin' ), + 'filter_items_list' => _x( 'Filter books list', 'Screen reader text for the filter links', 'your-plugin' ), + 'items_list_navigation' => _x( 'Books list navigation', 'Screen reader text for the pagination', 'your-plugin' ), + 'items_list' => _x( 'Books list', 'Screen reader text for the items list', 'your-plugin' ), + ); + + $args = array( + 'labels' => $labels, + 'description' => __( 'Books managed by your plugin', 'your-plugin' ), + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'books' ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => 20, + 'menu_icon' => 'dashicons-book', + 'show_in_rest' => true, // Enable Gutenberg editor + 'rest_base' => 'books', + 'rest_controller_class' => 'WP_REST_Posts_Controller', + 'supports' => array( + 'title', + 'editor', + 'author', + 'thumbnail', + 'excerpt', + 'comments', + 'custom-fields', + 'revisions', + ), + ); + + register_post_type( 'book', $args ); +} +add_action( 'init', 'yourprefix_register_book_post_type' ); + +/** + * Register custom taxonomy (Hierarchical - like Categories) + */ +function yourprefix_register_genre_taxonomy() { + $labels = array( + 'name' => _x( 'Genres', 'taxonomy general name', 'your-plugin' ), + 'singular_name' => _x( 'Genre', 'taxonomy singular name', 'your-plugin' ), + 'search_items' => __( 'Search Genres', 'your-plugin' ), + 'all_items' => __( 'All Genres', 'your-plugin' ), + 'parent_item' => __( 'Parent Genre', 'your-plugin' ), + 'parent_item_colon' => __( 'Parent Genre:', 'your-plugin' ), + 'edit_item' => __( 'Edit Genre', 'your-plugin' ), + 'update_item' => __( 'Update Genre', 'your-plugin' ), + 'add_new_item' => __( 'Add New Genre', 'your-plugin' ), + 'new_item_name' => __( 'New Genre Name', 'your-plugin' ), + 'menu_name' => __( 'Genre', 'your-plugin' ), + ); + + $args = array( + 'hierarchical' => true, // true = like categories, false = like tags + 'labels' => $labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'genre' ), + 'show_in_rest' => true, + ); + + register_taxonomy( 'genre', array( 'book' ), $args ); +} +add_action( 'init', 'yourprefix_register_genre_taxonomy' ); + +/** + * Register non-hierarchical taxonomy (like Tags) + */ +function yourprefix_register_book_tag_taxonomy() { + $labels = array( + 'name' => _x( 'Book Tags', 'taxonomy general name', 'your-plugin' ), + 'singular_name' => _x( 'Book Tag', 'taxonomy singular name', 'your-plugin' ), + 'search_items' => __( 'Search Book Tags', 'your-plugin' ), + 'popular_items' => __( 'Popular Book Tags', 'your-plugin' ), + 'all_items' => __( 'All Book Tags', 'your-plugin' ), + 'edit_item' => __( 'Edit Book Tag', 'your-plugin' ), + 'update_item' => __( 'Update Book Tag', 'your-plugin' ), + 'add_new_item' => __( 'Add New Book Tag', 'your-plugin' ), + 'new_item_name' => __( 'New Book Tag Name', 'your-plugin' ), + 'separate_items_with_commas' => __( 'Separate book tags with commas', 'your-plugin' ), + 'add_or_remove_items' => __( 'Add or remove book tags', 'your-plugin' ), + 'choose_from_most_used' => __( 'Choose from the most used book tags', 'your-plugin' ), + 'not_found' => __( 'No book tags found.', 'your-plugin' ), + 'menu_name' => __( 'Book Tags', 'your-plugin' ), + ); + + $args = array( + 'hierarchical' => false, // Non-hierarchical (like tags) + 'labels' => $labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'book-tag' ), + 'show_in_rest' => true, + ); + + register_taxonomy( 'book_tag', array( 'book' ), $args ); +} +add_action( 'init', 'yourprefix_register_book_tag_taxonomy' ); + +/** + * Customize archive page query for books + */ +function yourprefix_modify_book_archive_query( $query ) { + // Only modify main query on frontend for book archives + if ( ! is_admin() && $query->is_main_query() && is_post_type_archive( 'book' ) ) { + // Show 12 books per page + $query->set( 'posts_per_page', 12 ); + + // Order by title + $query->set( 'orderby', 'title' ); + $query->set( 'order', 'ASC' ); + } +} +add_action( 'pre_get_posts', 'yourprefix_modify_book_archive_query' ); + +/** + * Add custom columns to book admin list + */ +function yourprefix_add_book_columns( $columns ) { + $new_columns = array(); + + foreach ( $columns as $key => $value ) { + $new_columns[ $key ] = $value; + + // Add genre column after title + if ( 'title' === $key ) { + $new_columns['genre'] = __( 'Genres', 'your-plugin' ); + $new_columns['book_tags'] = __( 'Tags', 'your-plugin' ); + } + } + + return $new_columns; +} +add_filter( 'manage_book_posts_columns', 'yourprefix_add_book_columns' ); + +/** + * Populate custom columns + */ +function yourprefix_populate_book_columns( $column, $post_id ) { + if ( 'genre' === $column ) { + $genres = get_the_terms( $post_id, 'genre' ); + if ( $genres && ! is_wp_error( $genres ) ) { + $genre_list = array(); + foreach ( $genres as $genre ) { + $genre_list[] = sprintf( + '%s', + esc_url( add_query_arg( array( 'genre' => $genre->slug ), admin_url( 'edit.php?post_type=book' ) ) ), + esc_html( $genre->name ) + ); + } + echo implode( ', ', $genre_list ); + } else { + echo '—'; + } + } + + if ( 'book_tags' === $column ) { + $tags = get_the_terms( $post_id, 'book_tag' ); + if ( $tags && ! is_wp_error( $tags ) ) { + $tag_list = array(); + foreach ( $tags as $tag ) { + $tag_list[] = esc_html( $tag->name ); + } + echo implode( ', ', $tag_list ); + } else { + echo '—'; + } + } +} +add_action( 'manage_book_posts_custom_column', 'yourprefix_populate_book_columns', 10, 2 ); + +/** + * Make columns sortable + */ +function yourprefix_sortable_book_columns( $columns ) { + $columns['genre'] = 'genre'; + return $columns; +} +add_filter( 'manage_edit-book_sortable_columns', 'yourprefix_sortable_book_columns' ); + +/** + * Flush rewrite rules on plugin activation + */ +function yourprefix_activate() { + // Register post types and taxonomies + yourprefix_register_book_post_type(); + yourprefix_register_genre_taxonomy(); + yourprefix_register_book_tag_taxonomy(); + + // CRITICAL: Flush rewrite rules to prevent 404 errors + flush_rewrite_rules(); +} +register_activation_hook( __FILE__, 'yourprefix_activate' ); + +/** + * Flush rewrite rules on plugin deactivation + */ +function yourprefix_deactivate() { + // Flush rewrite rules + flush_rewrite_rules(); +} +register_deactivation_hook( __FILE__, 'yourprefix_deactivate' ); + +/** + * Query books by genre + * + * Example usage in theme templates + */ +function yourprefix_get_books_by_genre( $genre_slug, $posts_per_page = 10 ) { + $args = array( + 'post_type' => 'book', + 'posts_per_page' => $posts_per_page, + 'tax_query' => array( + array( + 'taxonomy' => 'genre', + 'field' => 'slug', + 'terms' => $genre_slug, + ), + ), + ); + + return new WP_Query( $args ); +} diff --git a/templates/examples/meta-box.php b/templates/examples/meta-box.php new file mode 100644 index 0000000..ee552fb --- /dev/null +++ b/templates/examples/meta-box.php @@ -0,0 +1,243 @@ +ID, '_yourprefix_text', true ); + $number_value = get_post_meta( $post->ID, '_yourprefix_number', true ); + $select_value = get_post_meta( $post->ID, '_yourprefix_select', true ); + $checkbox = get_post_meta( $post->ID, '_yourprefix_checkbox', true ); + + ?> + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ +

+
+ + + +
+ + + +
+ + + +
+ ID, '_yourprefix_text', true ); + $number_value = get_post_meta( $post->ID, '_yourprefix_number', true ); + $select_value = get_post_meta( $post->ID, '_yourprefix_select', true ); + $checkbox = get_post_meta( $post->ID, '_yourprefix_checkbox', true ); + + if ( $text_value || $number_value || $select_value || $checkbox ) { + $meta_html = ''; + + $content .= $meta_html; + } + + return $content; +} +add_filter( 'the_content', 'yourprefix_display_meta_data' ); diff --git a/templates/examples/rest-endpoint.php b/templates/examples/rest-endpoint.php new file mode 100644 index 0000000..4345fba --- /dev/null +++ b/templates/examples/rest-endpoint.php @@ -0,0 +1,384 @@ + 'GET', + 'callback' => 'yourprefix_get_items', + 'permission_callback' => '__return_true', // Public endpoint + ) + ); + + // GET /wp-json/yourplugin/v1/items/{id} + register_rest_route( + $namespace, + '/items/(?P\d+)', + array( + 'methods' => 'GET', + 'callback' => 'yourprefix_get_item', + 'permission_callback' => '__return_true', + 'args' => array( + 'id' => array( + 'required' => true, + 'validate_callback' => function( $param ) { + return is_numeric( $param ); + }, + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + + // POST /wp-json/yourplugin/v1/items + register_rest_route( + $namespace, + '/items', + array( + 'methods' => 'POST', + 'callback' => 'yourprefix_create_item', + 'permission_callback' => 'yourprefix_check_permission', + 'args' => yourprefix_get_item_args(), + ) + ); + + // PUT /wp-json/yourplugin/v1/items/{id} + register_rest_route( + $namespace, + '/items/(?P\d+)', + array( + 'methods' => 'PUT', + 'callback' => 'yourprefix_update_item', + 'permission_callback' => 'yourprefix_check_permission', + 'args' => array_merge( + array( + 'id' => array( + 'required' => true, + 'validate_callback' => function( $param ) { + return is_numeric( $param ); + }, + 'sanitize_callback' => 'absint', + ), + ), + yourprefix_get_item_args() + ), + ) + ); + + // DELETE /wp-json/yourplugin/v1/items/{id} + register_rest_route( + $namespace, + '/items/(?P\d+)', + array( + 'methods' => 'DELETE', + 'callback' => 'yourprefix_delete_item', + 'permission_callback' => 'yourprefix_check_permission', + 'args' => array( + 'id' => array( + 'required' => true, + 'validate_callback' => function( $param ) { + return is_numeric( $param ); + }, + 'sanitize_callback' => 'absint', + ), + ), + ) + ); +} +add_action( 'rest_api_init', 'yourprefix_register_rest_routes' ); + +/** + * Get all items + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error + */ +function yourprefix_get_items( $request ) { + // Get query parameters + $per_page = $request->get_param( 'per_page' ); + $page = $request->get_param( 'page' ); + + // Set defaults + $per_page = $per_page ? absint( $per_page ) : 10; + $page = $page ? absint( $page ) : 1; + + // Query posts + $args = array( + 'post_type' => 'post', + 'posts_per_page' => $per_page, + 'paged' => $page, + 'post_status' => 'publish', + ); + + $query = new WP_Query( $args ); + + $items = array(); + foreach ( $query->posts as $post ) { + $items[] = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'content' => $post->post_content, + 'excerpt' => $post->post_excerpt, + 'date' => $post->post_date, + 'author' => get_the_author_meta( 'display_name', $post->post_author ), + ); + } + + // Return response with pagination headers + $response = rest_ensure_response( $items ); + $response->header( 'X-WP-Total', $query->found_posts ); + $response->header( 'X-WP-TotalPages', $query->max_num_pages ); + + return $response; +} + +/** + * Get single item + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error + */ +function yourprefix_get_item( $request ) { + $item_id = $request->get_param( 'id' ); + $post = get_post( $item_id ); + + if ( ! $post ) { + return new WP_Error( + 'not_found', + __( 'Item not found', 'your-plugin' ), + array( 'status' => 404 ) + ); + } + + $data = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'content' => $post->post_content, + 'excerpt' => $post->post_excerpt, + 'date' => $post->post_date, + 'author' => get_the_author_meta( 'display_name', $post->post_author ), + ); + + return rest_ensure_response( $data ); +} + +/** + * Create item + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error + */ +function yourprefix_create_item( $request ) { + $title = $request->get_param( 'title' ); + $content = $request->get_param( 'content' ); + $status = $request->get_param( 'status' ); + + // Create post + $post_id = wp_insert_post( array( + 'post_title' => $title, + 'post_content' => $content, + 'post_status' => $status ? $status : 'draft', + 'post_type' => 'post', + ) ); + + if ( is_wp_error( $post_id ) ) { + return $post_id; + } + + $post = get_post( $post_id ); + + $data = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'content' => $post->post_content, + 'status' => $post->post_status, + 'date' => $post->post_date, + ); + + return rest_ensure_response( $data ); +} + +/** + * Update item + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error + */ +function yourprefix_update_item( $request ) { + $item_id = $request->get_param( 'id' ); + $post = get_post( $item_id ); + + if ( ! $post ) { + return new WP_Error( + 'not_found', + __( 'Item not found', 'your-plugin' ), + array( 'status' => 404 ) + ); + } + + // Update post + $update_data = array( + 'ID' => $item_id, + ); + + if ( $request->has_param( 'title' ) ) { + $update_data['post_title'] = $request->get_param( 'title' ); + } + + if ( $request->has_param( 'content' ) ) { + $update_data['post_content'] = $request->get_param( 'content' ); + } + + if ( $request->has_param( 'status' ) ) { + $update_data['post_status'] = $request->get_param( 'status' ); + } + + $result = wp_update_post( $update_data, true ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + $post = get_post( $item_id ); + + $data = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'content' => $post->post_content, + 'status' => $post->post_status, + 'date' => $post->post_date, + ); + + return rest_ensure_response( $data ); +} + +/** + * Delete item + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error + */ +function yourprefix_delete_item( $request ) { + $item_id = $request->get_param( 'id' ); + $post = get_post( $item_id ); + + if ( ! $post ) { + return new WP_Error( + 'not_found', + __( 'Item not found', 'your-plugin' ), + array( 'status' => 404 ) + ); + } + + // Delete post (move to trash) + $result = wp_trash_post( $item_id ); + + if ( ! $result ) { + return new WP_Error( + 'cannot_delete', + __( 'Could not delete item', 'your-plugin' ), + array( 'status' => 500 ) + ); + } + + return rest_ensure_response( array( + 'deleted' => true, + 'id' => $item_id, + ) ); +} + +/** + * Check permission + * + * @return bool + */ +function yourprefix_check_permission() { + return current_user_can( 'edit_posts' ); +} + +/** + * Get item validation arguments + * + * @return array + */ +function yourprefix_get_item_args() { + return array( + 'title' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Item title', 'your-plugin' ), + 'sanitize_callback' => 'sanitize_text_field', + ), + 'content' => array( + 'required' => false, + 'type' => 'string', + 'description' => __( 'Item content', 'your-plugin' ), + 'sanitize_callback' => 'wp_kses_post', + ), + 'status' => array( + 'required' => false, + 'type' => 'string', + 'description' => __( 'Item status', 'your-plugin' ), + 'enum' => array( 'publish', 'draft', 'pending', 'private' ), + 'sanitize_callback' => 'sanitize_text_field', + ), + ); +} + +/** + * Example AJAX endpoint (alternative to REST API for simpler use cases) + */ +function yourprefix_ajax_get_items() { + // Verify nonce + check_ajax_referer( 'yourprefix_nonce', 'nonce' ); + + // Check permission + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied', 'your-plugin' ) ) ); + } + + // Get items + $args = array( + 'post_type' => 'post', + 'posts_per_page' => 10, + 'post_status' => 'publish', + ); + + $query = new WP_Query( $args ); + + $items = array(); + foreach ( $query->posts as $post ) { + $items[] = array( + 'id' => $post->ID, + 'title' => $post->post_title, + ); + } + + wp_send_json_success( array( 'items' => $items ) ); +} +add_action( 'wp_ajax_yourprefix_get_items', 'yourprefix_ajax_get_items' ); diff --git a/templates/examples/settings-page.php b/templates/examples/settings-page.php new file mode 100644 index 0000000..c02ebea --- /dev/null +++ b/templates/examples/settings-page.php @@ -0,0 +1,308 @@ + +
+

+
+ +
+
+ ' . esc_html__( 'Configure your plugin settings below.', 'your-plugin' ) . '

'; +} + +/** + * Get settings with defaults + * + * @return array + */ +function yourprefix_get_settings() { + $defaults = array( + 'text_field' => '', + 'number_field' => 0, + 'checkbox_field' => false, + 'select_field' => '', + 'textarea_field' => '', + ); + + $settings = get_option( 'yourprefix_settings', $defaults ); + + return wp_parse_args( $settings, $defaults ); +} + +/** + * Text field callback + */ +function yourprefix_text_field_callback() { + $settings = yourprefix_get_settings(); + $value = $settings['text_field']; + + printf( + '', + esc_attr( $value ) + ); + echo '

' . esc_html__( 'Enter some text here', 'your-plugin' ) . '

'; +} + +/** + * Number field callback + */ +function yourprefix_number_field_callback() { + $settings = yourprefix_get_settings(); + $value = $settings['number_field']; + + printf( + '', + esc_attr( $value ) + ); + echo '

' . esc_html__( 'Enter a number between 0 and 100', 'your-plugin' ) . '

'; +} + +/** + * Checkbox field callback + */ +function yourprefix_checkbox_field_callback() { + $settings = yourprefix_get_settings(); + $value = $settings['checkbox_field']; + + printf( + '', + checked( $value, true, false ) + ); + echo ''; +} + +/** + * Select field callback + */ +function yourprefix_select_field_callback() { + $settings = yourprefix_get_settings(); + $value = $settings['select_field']; + + ?> + + %s', + esc_textarea( $value ) + ); + echo '

' . esc_html__( 'Enter some longer text here', 'your-plugin' ) . '

'; +} + +/** + * Sanitize settings + * + * @param array $input Input array. + * @return array Sanitized array. + */ +function yourprefix_sanitize_settings( $input ) { + $sanitized = array(); + + // Text field + if ( isset( $input['text_field'] ) ) { + $sanitized['text_field'] = sanitize_text_field( $input['text_field'] ); + } + + // Number field + if ( isset( $input['number_field'] ) ) { + $number = absint( $input['number_field'] ); + + // Validate range + if ( $number < 0 || $number > 100 ) { + add_settings_error( + 'yourprefix_messages', + 'yourprefix_message', + __( 'Number must be between 0 and 100', 'your-plugin' ), + 'error' + ); + $sanitized['number_field'] = 0; + } else { + $sanitized['number_field'] = $number; + } + } + + // Checkbox field + if ( isset( $input['checkbox_field'] ) ) { + $sanitized['checkbox_field'] = true; + } else { + $sanitized['checkbox_field'] = false; + } + + // Select field + if ( isset( $input['select_field'] ) ) { + $allowed_values = array( 'option1', 'option2', 'option3' ); + $select_value = sanitize_text_field( $input['select_field'] ); + + // Validate against allowed values + if ( in_array( $select_value, $allowed_values, true ) ) { + $sanitized['select_field'] = $select_value; + } else { + $sanitized['select_field'] = ''; + } + } + + // Textarea field + if ( isset( $input['textarea_field'] ) ) { + $sanitized['textarea_field'] = sanitize_textarea_field( $input['textarea_field'] ); + } + + return $sanitized; +} + +/** + * Get a specific setting value + * + * @param string $key Setting key. + * @param mixed $default Default value. + * @return mixed Setting value or default. + */ +function yourprefix_get_setting( $key, $default = '' ) { + $settings = yourprefix_get_settings(); + + return isset( $settings[ $key ] ) ? $settings[ $key ] : $default; +} diff --git a/templates/plugin-oop/README.md b/templates/plugin-oop/README.md new file mode 100644 index 0000000..2a5de52 --- /dev/null +++ b/templates/plugin-oop/README.md @@ -0,0 +1,285 @@ +# OOP WordPress Plugin Template + +This is an object-oriented singleton pattern for WordPress plugins. Best for medium to large plugins that need organized, maintainable architecture. + +## Features + +✅ Singleton pattern (single instance) +✅ Complete plugin header +✅ ABSPATH security check +✅ Class-based organization +✅ Custom post type and taxonomy +✅ Admin settings page with separate view template +✅ AJAX handlers (logged-in and public) +✅ REST API endpoints +✅ Proper activation/deactivation hooks +✅ Uninstall script for cleanup +✅ Internationalization ready +✅ Conditional asset loading +✅ Security best practices + +## Installation + +1. Copy this folder to `wp-content/plugins/` +2. Rename folder and files to match your plugin name +3. Find and replace the following: + - `My_OOP_Plugin` → Your_Plugin_Class_Name + - `My OOP Plugin` → Your plugin name + - `my-oop-plugin` → your-plugin-slug + - `myop_` → yourprefix_ + - `MYOP_` → YOURPREFIX_ + - `https://example.com` → Your website + - `Your Name` → Your name +4. Activate in WordPress admin + +## Structure + +``` +my-oop-plugin/ +├── my-oop-plugin.php # Main plugin file with class +├── uninstall.php # Cleanup on uninstall +├── README.md # This file +├── views/ # Template files +│ └── admin-settings.php # Settings page template +├── assets/ # CSS/JS files (create as needed) +│ ├── css/ +│ │ ├── admin-style.css +│ │ └── style.css +│ └── js/ +│ ├── admin-script.js +│ └── script.js +└── languages/ # Translation files (create as needed) +``` + +## Class Structure + +### Singleton Pattern +```php +My_OOP_Plugin::get_instance(); // Get single instance +``` + +### Key Methods + +**Initialization**: +- `__construct()` - Private constructor, sets up plugin +- `define_constants()` - Define plugin constants +- `init_hooks()` - Register WordPress hooks +- `init()` - Initialize functionality on `init` hook + +**Post Types & Taxonomies**: +- `register_post_types()` - Register custom post types +- `register_taxonomies()` - Register custom taxonomies + +**Admin**: +- `add_admin_menu()` - Add admin menu pages +- `render_settings_page()` - Render settings page +- `save_settings()` - Handle form submission +- `get_settings()` - Get plugin settings + +**Assets**: +- `admin_enqueue_scripts()` - Load admin assets +- `enqueue_scripts()` - Load frontend assets + +**AJAX**: +- `ajax_handler()` - Handle logged-in AJAX requests +- `ajax_handler_nopriv()` - Handle public AJAX requests +- `process_ajax_data()` - Process AJAX data + +**REST API**: +- `register_rest_routes()` - Register REST endpoints +- `rest_permission_check()` - Check permissions +- `rest_get_books()` - GET /wp-json/myop/v1/books +- `rest_get_book()` - GET /wp-json/myop/v1/books/{id} + +**Lifecycle**: +- `activate()` - Run on plugin activation +- `deactivate()` - Run on plugin deactivation + +## Included Examples + +### Custom Post Type & Taxonomy +- "Books" post type with Gutenberg support +- "Genres" hierarchical taxonomy + +### Settings Page +- Located in Settings → OOP Plugin +- Separate view template (views/admin-settings.php) +- Multiple field types (text, number, checkbox) +- Nonce verification and sanitization + +### AJAX Handlers +- Logged-in: `wp_ajax_myop_action` +- Public: `wp_ajax_nopriv_myop_action` +- Nonce verification and capability checking + +### REST API +- `GET /wp-json/myop/v1/books` - List all books +- `GET /wp-json/myop/v1/books/{id}` - Get single book +- Permission callbacks +- Validation and sanitization + +## Extending the Plugin + +### Add a New Method +```php +/** + * Your custom method + * + * @param string $param Parameter description + * @return mixed + */ +private function custom_method( $param ) { + // Your code here +} +``` + +### Add a New Hook +Add to `init_hooks()`: +```php +add_action( 'hook_name', array( $this, 'method_name' ) ); +``` + +### Add a New REST Endpoint +Add to `register_rest_routes()`: +```php +register_rest_route( + 'myop/v1', + '/endpoint', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'rest_endpoint_handler' ), + 'permission_callback' => array( $this, 'rest_permission_check' ), + ) +); +``` + +## Security Checklist + +- [x] ABSPATH check at top of file +- [x] Private constructor (singleton) +- [x] Prevent cloning and unserializing +- [x] Nonces for all forms +- [x] Capability checks (current_user_can) +- [x] Input sanitization (sanitize_text_field, absint) +- [x] Output escaping (esc_html, esc_attr) +- [x] AJAX nonce verification (check_ajax_referer) +- [x] REST API permission callbacks +- [x] REST API argument validation +- [x] Conditional asset loading + +## Advantages of OOP Pattern + +✅ **Organization** - Related code grouped in methods +✅ **Maintainability** - Easy to find and modify functionality +✅ **Encapsulation** - Private methods protect internal logic +✅ **Singleton** - Prevents multiple instances +✅ **Scalability** - Easy to extend with new methods +✅ **Testing** - Methods can be tested individually + +## When to Use OOP vs Simple + +**Use OOP when**: +- Plugin has 10+ functions +- Need organized, maintainable code +- Multiple developers working on plugin +- Plugin will grow over time +- Need private/protected methods + +**Use Simple when**: +- Plugin has <10 functions +- Simple, focused functionality +- One-person project +- Unlikely to grow significantly + +## Distribution & Auto-Updates + +### Enabling GitHub Auto-Updates + +You can provide automatic updates from GitHub without submitting to WordPress.org: + +**1. Install Plugin Update Checker library:** + +```bash +cd your-plugin/ +git submodule add https://github.com/YahnisElsts/plugin-update-checker.git +``` + +**2. Add to `init_hooks()` method:** + +```php +/** + * Include and initialize update checker + */ +private function init_updater() { + $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-slug' + ); + + $updateChecker->setBranch( 'main' ); + $updateChecker->getVcsApi()->enableReleaseAssets(); +} +``` + +Then call it in `init_hooks()`: + +```php +private function init_hooks() { + // Existing hooks... + + // Initialize auto-updates + $this->init_updater(); +} +``` + +**3. For private repos, add token to wp-config.php:** + +```php +define( 'YOUR_PLUGIN_GITHUB_TOKEN', 'ghp_xxxxxxxxxxxxx' ); +``` + +Then update `init_updater()`: + +```php +if ( defined( 'MYOP_GITHUB_TOKEN' ) ) { + $updateChecker->setAuthentication( MYOP_GITHUB_TOKEN ); +} +``` + +**4. Create releases:** + +```bash +# Update version in plugin header +git add my-oop-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 + +# Create GitHub Release with pre-built ZIP +``` + +### Resources + +- **Complete Guide**: See `references/github-auto-updates.md` +- **Implementation Examples**: See `examples/github-updater.php` +- **Plugin Update Checker**: https://github.com/YahnisElsts/plugin-update-checker + +## Resources + +- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/) +- [WordPress OOP Best Practices](https://developer.wordpress.org/plugins/plugin-basics/best-practices/) +- [Singleton Pattern](https://en.wikipedia.org/wiki/Singleton_pattern) + +## License + +GPL v2 or later diff --git a/templates/plugin-oop/my-oop-plugin.php b/templates/plugin-oop/my-oop-plugin.php new file mode 100644 index 0000000..e39cdcc --- /dev/null +++ b/templates/plugin-oop/my-oop-plugin.php @@ -0,0 +1,526 @@ +plugin_dir = plugin_dir_path( __FILE__ ); + $this->plugin_url = plugin_dir_url( __FILE__ ); + + $this->define_constants(); + $this->init_hooks(); + } + + /** + * Prevent cloning + */ + private function __clone() {} + + /** + * Prevent unserializing + */ + public function __wakeup() { + throw new Exception( 'Cannot unserialize singleton' ); + } + + /** + * Define plugin constants + */ + private function define_constants() { + define( 'MYOP_VERSION', self::VERSION ); + define( 'MYOP_PLUGIN_DIR', $this->plugin_dir ); + define( 'MYOP_PLUGIN_URL', $this->plugin_url ); + define( 'MYOP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); + } + + /** + * Initialize WordPress hooks + */ + private function init_hooks() { + // Core hooks + add_action( 'init', array( $this, 'init' ) ); + add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); + + // AJAX hooks + add_action( 'wp_ajax_myop_action', array( $this, 'ajax_handler' ) ); + add_action( 'wp_ajax_nopriv_myop_action', array( $this, 'ajax_handler_nopriv' ) ); + + // REST API hooks + add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); + + // Activation/Deactivation + register_activation_hook( __FILE__, array( $this, 'activate' ) ); + register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) ); + } + + /** + * Initialize plugin functionality + */ + public function init() { + // Load text domain + load_plugin_textdomain( + 'my-oop-plugin', + false, + dirname( MYOP_PLUGIN_BASENAME ) . '/languages' + ); + + // Register custom post type + $this->register_post_types(); + + // Register taxonomies + $this->register_taxonomies(); + } + + /** + * Register custom post types + */ + private function register_post_types() { + $labels = array( + 'name' => _x( 'Books', 'post type general name', 'my-oop-plugin' ), + 'singular_name' => _x( 'Book', 'post type singular name', 'my-oop-plugin' ), + 'menu_name' => _x( 'Books', 'admin menu', 'my-oop-plugin' ), + 'add_new' => _x( 'Add New', 'book', 'my-oop-plugin' ), + 'add_new_item' => __( 'Add New Book', 'my-oop-plugin' ), + 'edit_item' => __( 'Edit Book', 'my-oop-plugin' ), + 'new_item' => __( 'New Book', 'my-oop-plugin' ), + 'view_item' => __( 'View Book', 'my-oop-plugin' ), + 'search_items' => __( 'Search Books', 'my-oop-plugin' ), + 'not_found' => __( 'No books found', 'my-oop-plugin' ), + 'not_found_in_trash' => __( 'No books found in Trash', 'my-oop-plugin' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'books' ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => 5, + 'menu_icon' => 'dashicons-book', + 'show_in_rest' => true, + 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), + ); + + register_post_type( 'book', $args ); + } + + /** + * Register taxonomies + */ + private function register_taxonomies() { + $labels = array( + 'name' => _x( 'Genres', 'taxonomy general name', 'my-oop-plugin' ), + 'singular_name' => _x( 'Genre', 'taxonomy singular name', 'my-oop-plugin' ), + 'search_items' => __( 'Search Genres', 'my-oop-plugin' ), + 'all_items' => __( 'All Genres', 'my-oop-plugin' ), + 'parent_item' => __( 'Parent Genre', 'my-oop-plugin' ), + 'parent_item_colon' => __( 'Parent Genre:', 'my-oop-plugin' ), + 'edit_item' => __( 'Edit Genre', 'my-oop-plugin' ), + 'update_item' => __( 'Update Genre', 'my-oop-plugin' ), + 'add_new_item' => __( 'Add New Genre', 'my-oop-plugin' ), + 'new_item_name' => __( 'New Genre Name', 'my-oop-plugin' ), + 'menu_name' => __( 'Genres', 'my-oop-plugin' ), + ); + + $args = array( + 'hierarchical' => true, + 'labels' => $labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'genre' ), + 'show_in_rest' => true, + ); + + register_taxonomy( 'genre', array( 'book' ), $args ); + } + + /** + * Add admin menu pages + */ + public function add_admin_menu() { + add_options_page( + __( 'My OOP Plugin Settings', 'my-oop-plugin' ), + __( 'OOP Plugin', 'my-oop-plugin' ), + 'manage_options', + 'my-oop-plugin', + array( $this, 'render_settings_page' ) + ); + } + + /** + * Render settings page + */ + public function render_settings_page() { + // Check user capabilities + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + // Handle form submission + if ( isset( $_POST['myop_settings_submit'] ) ) { + $this->save_settings(); + } + + // Get current settings + $settings = $this->get_settings(); + + // Render page + include $this->plugin_dir . 'views/admin-settings.php'; + } + + /** + * Save plugin settings + */ + private function save_settings() { + // Verify nonce + if ( ! isset( $_POST['myop_settings_nonce'] ) || + ! wp_verify_nonce( $_POST['myop_settings_nonce'], 'myop_settings_action' ) ) { + wp_die( __( 'Security check failed', 'my-oop-plugin' ) ); + } + + // Sanitize and save settings + $settings = array( + 'option1' => isset( $_POST['myop_option1'] ) ? sanitize_text_field( $_POST['myop_option1'] ) : '', + 'option2' => isset( $_POST['myop_option2'] ) ? absint( $_POST['myop_option2'] ) : 0, + 'option3' => isset( $_POST['myop_option3'] ) ? (bool) $_POST['myop_option3'] : false, + ); + + update_option( 'myop_settings', $settings ); + + add_settings_error( + 'myop_messages', + 'myop_message', + __( 'Settings Saved', 'my-oop-plugin' ), + 'updated' + ); + } + + /** + * Get plugin settings + * + * @return array + */ + private function get_settings() { + $defaults = array( + 'option1' => '', + 'option2' => 0, + 'option3' => false, + ); + + $settings = get_option( 'myop_settings', $defaults ); + + return wp_parse_args( $settings, $defaults ); + } + + /** + * Enqueue admin scripts and styles + * + * @param string $hook Current admin page hook + */ + public function admin_enqueue_scripts( $hook ) { + // Only load on specific pages + if ( 'edit.php' !== $hook && 'post.php' !== $hook && 'post-new.php' !== $hook ) { + return; + } + + $screen = get_current_screen(); + if ( $screen && 'book' === $screen->post_type ) { + wp_enqueue_style( + 'myop-admin-style', + $this->plugin_url . 'assets/css/admin-style.css', + array(), + self::VERSION + ); + + wp_enqueue_script( + 'myop-admin-script', + $this->plugin_url . 'assets/js/admin-script.js', + array( 'jquery' ), + self::VERSION, + true + ); + + wp_localize_script( + 'myop-admin-script', + 'myopData', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'myop_ajax_nonce' ), + ) + ); + } + } + + /** + * Enqueue frontend scripts and styles + */ + public function enqueue_scripts() { + if ( is_singular( 'book' ) ) { + wp_enqueue_style( + 'myop-style', + $this->plugin_url . 'assets/css/style.css', + array(), + self::VERSION + ); + + wp_enqueue_script( + 'myop-script', + $this->plugin_url . 'assets/js/script.js', + array( 'jquery' ), + self::VERSION, + true + ); + } + } + + /** + * AJAX handler (for logged-in users) + */ + public function ajax_handler() { + // Verify nonce + check_ajax_referer( 'myop_ajax_nonce', 'nonce' ); + + // Check capability + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied', 'my-oop-plugin' ) ) ); + } + + // Get and sanitize input + $input = isset( $_POST['data'] ) ? sanitize_text_field( $_POST['data'] ) : ''; + + // Process data + $result = $this->process_ajax_data( $input ); + + // Return response + wp_send_json_success( array( + 'message' => __( 'Success!', 'my-oop-plugin' ), + 'data' => $result, + ) ); + } + + /** + * AJAX handler (for non-logged-in users) + */ + public function ajax_handler_nopriv() { + // For public AJAX requests + wp_send_json_error( array( 'message' => __( 'Login required', 'my-oop-plugin' ) ) ); + } + + /** + * Process AJAX data + * + * @param string $data Input data + * @return mixed + */ + private function process_ajax_data( $data ) { + // Process your data here + return $data; + } + + /** + * Register REST API routes + */ + public function register_rest_routes() { + register_rest_route( + 'myop/v1', + '/books', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'rest_get_books' ), + 'permission_callback' => array( $this, 'rest_permission_check' ), + ) + ); + + register_rest_route( + 'myop/v1', + '/books/(?P\d+)', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'rest_get_book' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'id' => array( + 'required' => true, + 'validate_callback' => function( $param ) { + return is_numeric( $param ); + }, + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + } + + /** + * REST API permission check + * + * @return bool + */ + public function rest_permission_check() { + return current_user_can( 'edit_posts' ); + } + + /** + * REST API endpoint: Get books + * + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error + */ + public function rest_get_books( $request ) { + $args = array( + 'post_type' => 'book', + 'posts_per_page' => 10, + 'post_status' => 'publish', + ); + + $books = get_posts( $args ); + + $data = array(); + foreach ( $books as $book ) { + $data[] = array( + 'id' => $book->ID, + 'title' => $book->post_title, + 'content' => $book->post_content, + 'excerpt' => $book->post_excerpt, + ); + } + + return rest_ensure_response( $data ); + } + + /** + * REST API endpoint: Get single book + * + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error + */ + public function rest_get_book( $request ) { + $book_id = $request->get_param( 'id' ); + $book = get_post( $book_id ); + + if ( ! $book || 'book' !== $book->post_type ) { + return new WP_Error( 'not_found', __( 'Book not found', 'my-oop-plugin' ), array( 'status' => 404 ) ); + } + + $data = array( + 'id' => $book->ID, + 'title' => $book->post_title, + 'content' => $book->post_content, + 'excerpt' => $book->post_excerpt, + ); + + return rest_ensure_response( $data ); + } + + /** + * Plugin activation + */ + public function activate() { + // Register post types and taxonomies + $this->register_post_types(); + $this->register_taxonomies(); + + // Flush rewrite rules + flush_rewrite_rules(); + + // Set default settings + if ( false === get_option( 'myop_settings' ) ) { + add_option( 'myop_settings', array( + 'option1' => '', + 'option2' => 0, + 'option3' => false, + ) ); + } + + // Set activation timestamp + if ( false === get_option( 'myop_activated_time' ) ) { + add_option( 'myop_activated_time', current_time( 'timestamp' ) ); + } + } + + /** + * Plugin deactivation + */ + public function deactivate() { + // Flush rewrite rules + flush_rewrite_rules(); + + // Clear scheduled events + wp_clear_scheduled_hook( 'myop_cron_event' ); + } +} + +// Initialize plugin +My_OOP_Plugin::get_instance(); diff --git a/templates/plugin-oop/uninstall.php b/templates/plugin-oop/uninstall.php new file mode 100644 index 0000000..79e9695 --- /dev/null +++ b/templates/plugin-oop/uninstall.php @@ -0,0 +1,47 @@ +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 ); +} +*/ diff --git a/templates/plugin-oop/views/admin-settings.php b/templates/plugin-oop/views/admin-settings.php new file mode 100644 index 0000000..6da73d5 --- /dev/null +++ b/templates/plugin-oop/views/admin-settings.php @@ -0,0 +1,87 @@ + + +
+

+ + + +
+ + + + + + + + + + + + + + + + + +
+ + + +

+ +

+
+ + + +

+ +

+
+ + +
+ +

+ +

+
+
+ + +
+
diff --git a/templates/plugin-psr4/README.md b/templates/plugin-psr4/README.md new file mode 100644 index 0000000..51a25e2 --- /dev/null +++ b/templates/plugin-psr4/README.md @@ -0,0 +1,406 @@ +# PSR-4 WordPress Plugin Template + +This is a modern PSR-4 autoloading pattern for WordPress plugins using Composer. Best for large, complex plugins that need professional architecture with namespaces and autoloading. + +## Features + +✅ PSR-4 autoloading with Composer +✅ Namespace organization (MyPSR4Plugin\*) +✅ Singleton pattern +✅ Modular class structure by domain +✅ Custom post type with meta boxes +✅ Custom taxonomy +✅ Admin settings using Settings API +✅ REST API endpoints (GET, POST) +✅ Asset management (admin + frontend) +✅ Proper activation/deactivation hooks +✅ Uninstall script for cleanup +✅ Internationalization ready +✅ WordPress Coding Standards ready (PHPCS) +✅ Security best practices + +## Installation + +1. Copy this folder to `wp-content/plugins/` +2. Rename folder and files to match your plugin name +3. Find and replace the following: + - `MyPSR4Plugin` → YourPluginNamespace + - `My PSR-4 Plugin` → Your plugin name + - `my-psr4-plugin` → your-plugin-slug + - `mypp_` → yourprefix_ + - `MYPP_` → YOURPREFIX_ + - `yourname/my-psr4-plugin` → your-composer/package-name + - `https://example.com` → Your website + - `Your Name` → Your name +4. Run `composer install` in the plugin directory +5. Activate in WordPress admin + +## Structure + +``` +my-psr4-plugin/ +├── my-psr4-plugin.php # Main plugin file (bootstrapper) +├── composer.json # Composer dependencies and autoloading +├── uninstall.php # Cleanup on uninstall +├── README.md # This file +├── src/ # Source code (PSR-4 autoloaded) +│ ├── Plugin.php # Main plugin class +│ ├── PostTypes/ # Custom post types +│ │ └── Book.php +│ ├── Taxonomies/ # Taxonomies +│ │ └── Genre.php +│ ├── Admin/ # Admin functionality +│ │ └── Settings.php +│ ├── Frontend/ # Frontend functionality +│ │ └── Assets.php +│ └── API/ # REST API endpoints +│ └── BookEndpoints.php +├── assets/ # CSS/JS files (create as needed) +│ ├── css/ +│ │ ├── admin-style.css +│ │ └── style.css +│ └── js/ +│ ├── admin-script.js +│ └── script.js +├── languages/ # Translation files (create as needed) +└── vendor/ # Composer dependencies (auto-generated) +``` + +## Composer Setup + +### Install Dependencies + +```bash +cd wp-content/plugins/my-psr4-plugin +composer install +``` + +### Development Dependencies + +The template includes WordPress Coding Standards (WPCS) for code quality: + +```bash +# Check code standards +composer phpcs + +# Fix code standards automatically +composer phpcbf +``` + +### Update Dependencies + +```bash +composer update +``` + +## Namespaces + +The plugin uses the following namespace structure: + +- `MyPSR4Plugin\` - Root namespace +- `MyPSR4Plugin\PostTypes\` - Custom post types +- `MyPSR4Plugin\Taxonomies\` - Taxonomies +- `MyPSR4Plugin\Admin\` - Admin functionality +- `MyPSR4Plugin\Frontend\` - Frontend functionality +- `MyPSR4Plugin\API\` - REST API endpoints + +## Included Examples + +### Custom Post Type (src/PostTypes/Book.php) +- "Books" post type with Gutenberg support +- Meta boxes for ISBN, Author, Publication Year +- Nonce verification and sanitization + +### Taxonomy (src/Taxonomies/Genre.php) +- "Genres" hierarchical taxonomy +- Linked to Books post type + +### Admin Settings (src/Admin/Settings.php) +- Located in Settings → PSR-4 Plugin +- Uses WordPress Settings API +- Multiple field types with validation + +### REST API (src/API/BookEndpoints.php) +- `GET /wp-json/mypp/v1/books` - List all books +- `GET /wp-json/mypp/v1/books/{id}` - Get single book +- `POST /wp-json/mypp/v1/books` - Create book +- Permission callbacks and validation + +### Assets (src/Frontend/Assets.php) +- Conditional loading (admin + frontend) +- Localized script data + +## Adding New Classes + +### 1. Create Class File + +Create `src/YourNamespace/YourClass.php`: + +```php +init_updater(); + } + + private function init_updater() { + $updater_path = plugin_dir_path( __DIR__ ) . 'vendor/yahnis-elsts/plugin-update-checker/plugin-update-checker.php'; + + if ( ! file_exists( $updater_path ) ) { + return; + } + + require $updater_path; + + $this->updateChecker = PucFactory::buildUpdateChecker( + 'https://github.com/yourusername/your-plugin/', + plugin_dir_path( __DIR__ ) . 'my-psr4-plugin.php', + 'your-plugin-slug' + ); + + $this->updateChecker->setBranch( 'main' ); + $this->updateChecker->getVcsApi()->enableReleaseAssets(); + + // Private repo authentication + if ( defined( 'MYPP_GITHUB_TOKEN' ) ) { + $this->updateChecker->setAuthentication( MYPP_GITHUB_TOKEN ); + } + } +} +``` + +**3. Initialize in Plugin.php:** + +Add to `src/Plugin.php` `init()` method: + +```php +Updater::get_instance(); +``` + +**4. For private repos, add token to wp-config.php:** + +```php +define( 'MYPP_GITHUB_TOKEN', 'ghp_xxxxxxxxxxxxx' ); +``` + +**5. Create releases:** + +```bash +# Update version in plugin header +git add my-psr4-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 + +# Create GitHub Release with pre-built ZIP +``` + +**6. Build release ZIP:** + +```bash +#!/bin/bash +# build-release.sh + +VERSION="1.0.1" +PLUGIN_SLUG="my-psr4-plugin" + +# Install dependencies (no dev packages) +composer install --no-dev --optimize-autoloader + +# Create ZIP +zip -r "${PLUGIN_SLUG}-${VERSION}.zip" \ + "${PLUGIN_SLUG}/" \ + -x "*.git*" "*.github/*" "tests/*" "node_modules/*" + +echo "Built: ${PLUGIN_SLUG}-${VERSION}.zip" +``` + +### Resources + +- **Complete Guide**: See `references/github-auto-updates.md` +- **Implementation Examples**: See `examples/github-updater.php` +- **Plugin Update Checker**: https://github.com/YahnisElsts/plugin-update-checker + +## Resources + +- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/) +- [PSR-4 Autoloading](https://www.php-fig.org/psr/psr-4/) +- [Composer Documentation](https://getcomposer.org/doc/) +- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/) + +## License + +GPL v2 or later diff --git a/templates/plugin-psr4/composer.json b/templates/plugin-psr4/composer.json new file mode 100644 index 0000000..2509fe5 --- /dev/null +++ b/templates/plugin-psr4/composer.json @@ -0,0 +1,33 @@ +{ + "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 + } + } +} diff --git a/templates/plugin-psr4/my-psr4-plugin.php b/templates/plugin-psr4/my-psr4-plugin.php new file mode 100644 index 0000000..0613898 --- /dev/null +++ b/templates/plugin-psr4/my-psr4-plugin.php @@ -0,0 +1,58 @@ + +
+

+ composer install' + ); + ?> +

+
+ 'GET', + 'callback' => array( $this, 'get_books' ), + 'permission_callback' => '__return_true', + ) + ); + + // Get single book + register_rest_route( + self::NAMESPACE, + '/books/(?P\d+)', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_book' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'id' => array( + 'required' => true, + 'validate_callback' => function( $param ) { + return is_numeric( $param ); + }, + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + + // Create book + register_rest_route( + self::NAMESPACE, + '/books', + array( + 'methods' => 'POST', + 'callback' => array( $this, 'create_book' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => $this->get_book_args(), + ) + ); + } + + /** + * Get books + * + * @param \WP_REST_Request $request Request object. + * @return \WP_REST_Response|\WP_Error + */ + public function get_books( $request ) { + $args = array( + 'post_type' => Book::POST_TYPE, + 'posts_per_page' => 10, + 'post_status' => 'publish', + ); + + $books = get_posts( $args ); + + $data = array(); + foreach ( $books as $book ) { + $data[] = $this->prepare_book_data( $book ); + } + + return rest_ensure_response( $data ); + } + + /** + * Get single book + * + * @param \WP_REST_Request $request Request object. + * @return \WP_REST_Response|\WP_Error + */ + public function get_book( $request ) { + $book_id = $request->get_param( 'id' ); + $book = get_post( $book_id ); + + if ( ! $book || Book::POST_TYPE !== $book->post_type ) { + return new \WP_Error( + 'not_found', + __( 'Book not found', 'my-psr4-plugin' ), + array( 'status' => 404 ) + ); + } + + $data = $this->prepare_book_data( $book ); + + return rest_ensure_response( $data ); + } + + /** + * Create book + * + * @param \WP_REST_Request $request Request object. + * @return \WP_REST_Response|\WP_Error + */ + public function create_book( $request ) { + $title = $request->get_param( 'title' ); + $content = $request->get_param( 'content' ); + $isbn = $request->get_param( 'isbn' ); + $author = $request->get_param( 'author' ); + $year = $request->get_param( 'year' ); + + $post_id = wp_insert_post( array( + 'post_title' => $title, + 'post_content' => $content, + 'post_type' => Book::POST_TYPE, + 'post_status' => 'draft', + ) ); + + if ( is_wp_error( $post_id ) ) { + return $post_id; + } + + // Save meta data + if ( $isbn ) { + update_post_meta( $post_id, '_mypp_isbn', $isbn ); + } + if ( $author ) { + update_post_meta( $post_id, '_mypp_author', $author ); + } + if ( $year ) { + update_post_meta( $post_id, '_mypp_year', $year ); + } + + $book = get_post( $post_id ); + $data = $this->prepare_book_data( $book ); + + return rest_ensure_response( $data ); + } + + /** + * Prepare book data for response + * + * @param \WP_Post $book Post object. + * @return array + */ + private function prepare_book_data( $book ) { + return array( + 'id' => $book->ID, + 'title' => $book->post_title, + 'content' => $book->post_content, + 'excerpt' => $book->post_excerpt, + 'isbn' => get_post_meta( $book->ID, '_mypp_isbn', true ), + 'author' => get_post_meta( $book->ID, '_mypp_author', true ), + 'year' => (int) get_post_meta( $book->ID, '_mypp_year', true ), + ); + } + + /** + * Check permission + * + * @return bool + */ + public function check_permission() { + return current_user_can( 'edit_posts' ); + } + + /** + * Get book arguments for validation + * + * @return array + */ + private function get_book_args() { + return array( + 'title' => array( + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'content' => array( + 'required' => false, + 'type' => 'string', + 'sanitize_callback' => 'wp_kses_post', + ), + 'isbn' => array( + 'required' => false, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'author' => array( + 'required' => false, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'year' => array( + 'required' => false, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + ); + } +} diff --git a/templates/plugin-psr4/src/Admin/Settings.php b/templates/plugin-psr4/src/Admin/Settings.php new file mode 100644 index 0000000..9981f96 --- /dev/null +++ b/templates/plugin-psr4/src/Admin/Settings.php @@ -0,0 +1,248 @@ + 'option1' ) + ); + + add_settings_field( + 'mypp_option2', + __( 'Number Option', 'my-psr4-plugin' ), + array( $this, 'render_number_field' ), + 'my-psr4-plugin', + 'mypp_general_section', + array( 'field_id' => 'option2' ) + ); + + add_settings_field( + 'mypp_option3', + __( 'Checkbox Option', 'my-psr4-plugin' ), + array( $this, 'render_checkbox_field' ), + 'my-psr4-plugin', + 'mypp_general_section', + array( 'field_id' => 'option3' ) + ); + } + + /** + * Render settings page + */ + public function render_settings_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + ?> +
+

+
+ +
+
+ ' . esc_html__( 'Configure your plugin settings below.', 'my-psr4-plugin' ) . '

'; + } + + /** + * Render text field + * + * @param array $args Field arguments. + */ + public function render_text_field( $args ) { + $settings = $this->get_settings(); + $field_id = $args['field_id']; + $value = isset( $settings[ $field_id ] ) ? $settings[ $field_id ] : ''; + + printf( + '', + esc_attr( $field_id ), + esc_attr( self::OPTION_NAME ), + esc_attr( $value ) + ); + } + + /** + * Render number field + * + * @param array $args Field arguments. + */ + public function render_number_field( $args ) { + $settings = $this->get_settings(); + $field_id = $args['field_id']; + $value = isset( $settings[ $field_id ] ) ? $settings[ $field_id ] : 0; + + printf( + '', + esc_attr( $field_id ), + esc_attr( self::OPTION_NAME ), + esc_attr( $value ) + ); + } + + /** + * Render checkbox field + * + * @param array $args Field arguments. + */ + public function render_checkbox_field( $args ) { + $settings = $this->get_settings(); + $field_id = $args['field_id']; + $value = isset( $settings[ $field_id ] ) ? $settings[ $field_id ] : false; + + printf( + '', + esc_attr( $field_id ), + esc_attr( self::OPTION_NAME ), + checked( $value, true, false ) + ); + } + + /** + * Sanitize settings + * + * @param array $input Input array. + * @return array + */ + public function sanitize_settings( $input ) { + $sanitized = array(); + + if ( isset( $input['option1'] ) ) { + $sanitized['option1'] = sanitize_text_field( $input['option1'] ); + } + + if ( isset( $input['option2'] ) ) { + $sanitized['option2'] = absint( $input['option2'] ); + } + + if ( isset( $input['option3'] ) ) { + $sanitized['option3'] = (bool) $input['option3']; + } + + return $sanitized; + } + + /** + * Get settings + * + * @return array + */ + public function get_settings() { + $defaults = array( + 'option1' => '', + 'option2' => 0, + 'option3' => false, + ); + + $settings = get_option( self::OPTION_NAME, $defaults ); + + return wp_parse_args( $settings, $defaults ); + } + + /** + * Set default settings on activation + */ + public function set_defaults() { + if ( false === get_option( self::OPTION_NAME ) ) { + add_option( self::OPTION_NAME, array( + 'option1' => '', + 'option2' => 0, + 'option3' => false, + ) ); + } + } +} diff --git a/templates/plugin-psr4/src/Frontend/Assets.php b/templates/plugin-psr4/src/Frontend/Assets.php new file mode 100644 index 0000000..78794e4 --- /dev/null +++ b/templates/plugin-psr4/src/Frontend/Assets.php @@ -0,0 +1,111 @@ +post_type ) { + wp_enqueue_style( + 'mypp-admin-style', + MYPP_PLUGIN_URL . 'assets/css/admin-style.css', + array(), + MYPP_VERSION + ); + + wp_enqueue_script( + 'mypp-admin-script', + MYPP_PLUGIN_URL . 'assets/js/admin-script.js', + array( 'jquery' ), + MYPP_VERSION, + true + ); + + // Localize script + wp_localize_script( + 'mypp-admin-script', + 'myppData', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'mypp_ajax_nonce' ), + ) + ); + } + } +} diff --git a/templates/plugin-psr4/src/Plugin.php b/templates/plugin-psr4/src/Plugin.php new file mode 100644 index 0000000..5d96dc1 --- /dev/null +++ b/templates/plugin-psr4/src/Plugin.php @@ -0,0 +1,130 @@ +init_hooks(); + } + + /** + * Prevent cloning + */ + private function __clone() {} + + /** + * Prevent unserializing + */ + public function __wakeup() { + throw new \Exception( 'Cannot unserialize singleton' ); + } + + /** + * Initialize WordPress hooks + */ + private function init_hooks() { + add_action( 'init', array( $this, 'init' ) ); + add_action( 'admin_menu', array( $this, 'register_admin_pages' ) ); + add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); + } + + /** + * Initialize plugin + */ + public function init() { + // Load text domain + load_plugin_textdomain( + 'my-psr4-plugin', + false, + dirname( MYPP_PLUGIN_BASENAME ) . '/languages' + ); + + // Initialize submodules + PostTypes\Book::get_instance(); + Taxonomies\Genre::get_instance(); + Admin\Settings::get_instance(); + Frontend\Assets::get_instance(); + API\BookEndpoints::get_instance(); + } + + /** + * Register admin pages + */ + public function register_admin_pages() { + // Admin pages are handled by Admin\Settings class + } + + /** + * Register REST routes + */ + public function register_rest_routes() { + // REST routes are handled by API\BookEndpoints class + } + + /** + * Plugin activation + */ + public function activate() { + // Register post types and taxonomies + PostTypes\Book::get_instance()->register(); + Taxonomies\Genre::get_instance()->register(); + + // Flush rewrite rules + flush_rewrite_rules(); + + // Set default options + Admin\Settings::get_instance()->set_defaults(); + + // Set activation timestamp + if ( false === get_option( 'mypp_activated_time' ) ) { + add_option( 'mypp_activated_time', current_time( 'timestamp' ) ); + } + } + + /** + * Plugin deactivation + */ + public function deactivate() { + // Flush rewrite rules + flush_rewrite_rules(); + + // Clear scheduled events + wp_clear_scheduled_hook( 'mypp_cron_event' ); + } +} diff --git a/templates/plugin-psr4/src/PostTypes/Book.php b/templates/plugin-psr4/src/PostTypes/Book.php new file mode 100644 index 0000000..79ef081 --- /dev/null +++ b/templates/plugin-psr4/src/PostTypes/Book.php @@ -0,0 +1,215 @@ + _x( 'Books', 'post type general name', 'my-psr4-plugin' ), + 'singular_name' => _x( 'Book', 'post type singular name', 'my-psr4-plugin' ), + 'menu_name' => _x( 'Books', 'admin menu', 'my-psr4-plugin' ), + 'add_new' => _x( 'Add New', 'book', 'my-psr4-plugin' ), + 'add_new_item' => __( 'Add New Book', 'my-psr4-plugin' ), + 'edit_item' => __( 'Edit Book', 'my-psr4-plugin' ), + 'new_item' => __( 'New Book', 'my-psr4-plugin' ), + 'view_item' => __( 'View Book', 'my-psr4-plugin' ), + 'search_items' => __( 'Search Books', 'my-psr4-plugin' ), + 'not_found' => __( 'No books found', 'my-psr4-plugin' ), + 'not_found_in_trash' => __( 'No books found in Trash', 'my-psr4-plugin' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'books' ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => 5, + 'menu_icon' => 'dashicons-book', + 'show_in_rest' => true, + 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), + ); + + register_post_type( self::POST_TYPE, $args ); + } + + /** + * Add meta boxes + */ + public function add_meta_boxes() { + add_meta_box( + 'mypp_book_details', + __( 'Book Details', 'my-psr4-plugin' ), + array( $this, 'render_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + } + + /** + * Render meta box + * + * @param \WP_Post $post Post object. + */ + public function render_meta_box( $post ) { + // Add nonce for security + wp_nonce_field( 'mypp_save_book_meta', 'mypp_book_meta_nonce' ); + + // Get current values + $isbn = get_post_meta( $post->ID, '_mypp_isbn', true ); + $author = get_post_meta( $post->ID, '_mypp_author', true ); + $year = get_post_meta( $post->ID, '_mypp_year', true ); + + ?> + + + + + + + + + + + + + +
+ +
+ +
+ +
+ _x( 'Genres', 'taxonomy general name', 'my-psr4-plugin' ), + 'singular_name' => _x( 'Genre', 'taxonomy singular name', 'my-psr4-plugin' ), + 'search_items' => __( 'Search Genres', 'my-psr4-plugin' ), + 'all_items' => __( 'All Genres', 'my-psr4-plugin' ), + 'parent_item' => __( 'Parent Genre', 'my-psr4-plugin' ), + 'parent_item_colon' => __( 'Parent Genre:', 'my-psr4-plugin' ), + 'edit_item' => __( 'Edit Genre', 'my-psr4-plugin' ), + 'update_item' => __( 'Update Genre', 'my-psr4-plugin' ), + 'add_new_item' => __( 'Add New Genre', 'my-psr4-plugin' ), + 'new_item_name' => __( 'New Genre Name', 'my-psr4-plugin' ), + 'menu_name' => __( 'Genres', 'my-psr4-plugin' ), + ); + + $args = array( + 'hierarchical' => true, + 'labels' => $labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'genre' ), + 'show_in_rest' => true, + ); + + register_taxonomy( self::TAXONOMY, array( Book::POST_TYPE ), $args ); + } +} diff --git a/templates/plugin-psr4/uninstall.php b/templates/plugin-psr4/uninstall.php new file mode 100644 index 0000000..15e73fc --- /dev/null +++ b/templates/plugin-psr4/uninstall.php @@ -0,0 +1,47 @@ +get_col( "SELECT blog_id FROM $wpdb->blogs" ); + + foreach ( $blog_ids as $blog_id ) { + switch_to_blog( $blog_id ); + + delete_option( 'mypp_settings' ); + delete_option( 'mypp_activated_time' ); + delete_transient( 'mypp_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 ); +} +*/ diff --git a/templates/plugin-simple/README.md b/templates/plugin-simple/README.md new file mode 100644 index 0000000..5d88877 --- /dev/null +++ b/templates/plugin-simple/README.md @@ -0,0 +1,181 @@ +# Simple WordPress Plugin Template + +This is a functional programming pattern for WordPress plugins. Best for small to medium plugins that don't require complex object-oriented architecture. + +## Features + +✅ Complete plugin header with all fields +✅ ABSPATH security check +✅ Unique function prefix (mysp_) +✅ Custom post type registration (Books) +✅ Admin settings page with nonce verification +✅ AJAX handler with security checks +✅ Proper activation/deactivation hooks +✅ Uninstall script for cleanup +✅ Internationalization ready +✅ Conditional asset loading +✅ Security best practices (sanitization, escaping, capability checks) + +## Installation + +1. Copy this folder to `wp-content/plugins/` +2. Rename folder and files to match your plugin name +3. Find and replace the following: + - `My Simple Plugin` → Your plugin name + - `my-simple-plugin` → your-plugin-slug + - `mysp_` → yourprefix_ + - `MYSP_` → YOURPREFIX_ + - `https://example.com` → Your website + - `Your Name` → Your name +4. Activate in WordPress admin + +## Structure + +``` +my-simple-plugin/ +├── my-simple-plugin.php # Main plugin file +├── uninstall.php # Cleanup on uninstall +├── README.md # This file +├── assets/ # CSS/JS files (create as needed) +│ ├── css/ +│ │ ├── admin-style.css +│ │ └── style.css +│ └── js/ +│ ├── admin-script.js +│ └── script.js +└── languages/ # Translation files (create as needed) +``` + +## Included Examples + +### Custom Post Type +- Registers "Books" post type +- Gutenberg-enabled +- Archive page support +- Custom rewrite slug + +### Settings Page +- Located in Settings → Simple Plugin +- Nonce verification +- Sanitization and validation +- Settings error handling + +### AJAX Handler +- Action: `wp_ajax_mysp_action` +- Nonce verification +- Capability checking +- JSON response + +### Activation/Deactivation +- Flushes rewrite rules +- Sets default options +- Cleans up scheduled events + +### Uninstall +- Deletes all plugin options +- Clears transients +- Multisite support +- Optional: Delete custom post type data + +## Security Checklist + +- [x] ABSPATH check at top of file +- [x] Unique function prefix (mysp_) +- [x] Nonces for all forms +- [x] Capability checks (current_user_can) +- [x] Input sanitization (sanitize_text_field) +- [x] Output escaping (esc_html, esc_attr) +- [x] AJAX nonce verification (check_ajax_referer) +- [x] Conditional asset loading (don't load everywhere) +- [x] Proper uninstall cleanup + +## Next Steps + +1. Create the `assets/` directory structure +2. Add your CSS and JavaScript files +3. Extend with additional features: + - Meta boxes + - Shortcodes + - Widgets + - REST API endpoints + - Custom taxonomies + - WP-CLI commands + +## Distribution & Auto-Updates + +### Enabling GitHub Auto-Updates + +You can provide automatic updates from GitHub without submitting to WordPress.org: + +**1. Install Plugin Update Checker library:** + +```bash +cd your-plugin/ +git submodule add https://github.com/YahnisElsts/plugin-update-checker.git +``` + +**2. Add to your main plugin file:** + +```php +// Include Plugin Update Checker +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__, + 'your-plugin-slug' +); + +// Set branch (default: main) +$updateChecker->setBranch( 'main' ); + +// Use GitHub Releases (recommended) +$updateChecker->getVcsApi()->enableReleaseAssets(); +``` + +**3. For private repos, add token to wp-config.php:** + +```php +define( 'YOUR_PLUGIN_GITHUB_TOKEN', 'ghp_xxxxxxxxxxxxx' ); +``` + +Then in your plugin: + +```php +if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) { + $updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN ); +} +``` + +**4. Create releases:** + +```bash +# Update version in plugin header +git add my-simple-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 + +# Create GitHub Release (optional but recommended) +# - Upload pre-built ZIP file +# - Add release notes +``` + +### Resources + +- **Complete Guide**: See `references/github-auto-updates.md` +- **Implementation Examples**: See `examples/github-updater.php` +- **Plugin Update Checker**: https://github.com/YahnisElsts/plugin-update-checker + +## Resources + +- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/) +- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/) +- [Plugin Security](https://developer.wordpress.org/apis/security/) + +## License + +GPL v2 or later diff --git a/templates/plugin-simple/my-simple-plugin.php b/templates/plugin-simple/my-simple-plugin.php new file mode 100644 index 0000000..4423ef3 --- /dev/null +++ b/templates/plugin-simple/my-simple-plugin.php @@ -0,0 +1,272 @@ + _x( 'Books', 'post type general name', 'my-simple-plugin' ), + 'singular_name' => _x( 'Book', 'post type singular name', 'my-simple-plugin' ), + 'menu_name' => _x( 'Books', 'admin menu', 'my-simple-plugin' ), + 'add_new' => _x( 'Add New', 'book', 'my-simple-plugin' ), + 'add_new_item' => __( 'Add New Book', 'my-simple-plugin' ), + 'edit_item' => __( 'Edit Book', 'my-simple-plugin' ), + 'new_item' => __( 'New Book', 'my-simple-plugin' ), + 'view_item' => __( 'View Book', 'my-simple-plugin' ), + 'search_items' => __( 'Search Books', 'my-simple-plugin' ), + 'not_found' => __( 'No books found', 'my-simple-plugin' ), + 'not_found_in_trash' => __( 'No books found in Trash', 'my-simple-plugin' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'books' ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => 5, + 'menu_icon' => 'dashicons-book', + 'show_in_rest' => true, // Enable Gutenberg editor + 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), + ); + + register_post_type( 'book', $args ); +} + +/** + * Enqueue admin scripts and styles + */ +function mysp_admin_enqueue_scripts( $hook ) { + // Only load on specific admin pages + if ( 'edit.php' !== $hook && 'post.php' !== $hook && 'post-new.php' !== $hook ) { + return; + } + + $screen = get_current_screen(); + if ( $screen && 'book' === $screen->post_type ) { + wp_enqueue_style( + 'mysp-admin-style', + MYSP_PLUGIN_URL . 'assets/css/admin-style.css', + array(), + MYSP_VERSION + ); + + wp_enqueue_script( + 'mysp-admin-script', + MYSP_PLUGIN_URL . 'assets/js/admin-script.js', + array( 'jquery' ), + MYSP_VERSION, + true + ); + + // Localize script with nonce and AJAX URL + wp_localize_script( + 'mysp-admin-script', + 'myspData', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'mysp_ajax_nonce' ), + ) + ); + } +} +add_action( 'admin_enqueue_scripts', 'mysp_admin_enqueue_scripts' ); + +/** + * Enqueue frontend scripts and styles + */ +function mysp_enqueue_scripts() { + // Only load on single book pages + if ( is_singular( 'book' ) ) { + wp_enqueue_style( + 'mysp-style', + MYSP_PLUGIN_URL . 'assets/css/style.css', + array(), + MYSP_VERSION + ); + + wp_enqueue_script( + 'mysp-script', + MYSP_PLUGIN_URL . 'assets/js/script.js', + array( 'jquery' ), + MYSP_VERSION, + true + ); + } +} +add_action( 'wp_enqueue_scripts', 'mysp_enqueue_scripts' ); + +/** + * Add settings page to admin menu + */ +function mysp_add_settings_page() { + add_options_page( + __( 'My Simple Plugin Settings', 'my-simple-plugin' ), + __( 'Simple Plugin', 'my-simple-plugin' ), + 'manage_options', + 'my-simple-plugin', + 'mysp_render_settings_page' + ); +} +add_action( 'admin_menu', 'mysp_add_settings_page' ); + +/** + * Render settings page + */ +function mysp_render_settings_page() { + // Check user capabilities + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + // Handle form submission + if ( isset( $_POST['mysp_settings_submit'] ) ) { + // Verify nonce + if ( ! isset( $_POST['mysp_settings_nonce'] ) || ! wp_verify_nonce( $_POST['mysp_settings_nonce'], 'mysp_settings_action' ) ) { + wp_die( __( 'Security check failed', 'my-simple-plugin' ) ); + } + + // Sanitize and save option + $option_value = isset( $_POST['mysp_option'] ) ? sanitize_text_field( $_POST['mysp_option'] ) : ''; + update_option( 'mysp_option', $option_value ); + + // Show success message + add_settings_error( + 'mysp_messages', + 'mysp_message', + __( 'Settings Saved', 'my-simple-plugin' ), + 'updated' + ); + } + + // Get current option value + $option_value = get_option( 'mysp_option', '' ); + + // Display settings page + ?> +
+

+ +
+ + + + + + +
+ + + +

+
+ +
+
+ __( 'Permission denied', 'my-simple-plugin' ) ) ); + } + + // Get and sanitize input + $input = isset( $_POST['data'] ) ? sanitize_text_field( $_POST['data'] ) : ''; + + // Process and return response + wp_send_json_success( array( + 'message' => __( 'Success!', 'my-simple-plugin' ), + 'data' => $input, + ) ); +} +add_action( 'wp_ajax_mysp_action', 'mysp_ajax_handler' ); + +/** + * Activation hook + */ +function mysp_activate() { + // Register post type before flushing rewrite rules + mysp_register_book_post_type(); + + // Flush rewrite rules + flush_rewrite_rules(); + + // Set default options + if ( false === get_option( 'mysp_option' ) ) { + add_option( 'mysp_option', '' ); + } + + // Set activation timestamp + if ( false === get_option( 'mysp_activated_time' ) ) { + add_option( 'mysp_activated_time', current_time( 'timestamp' ) ); + } +} +register_activation_hook( __FILE__, 'mysp_activate' ); + +/** + * Deactivation hook + */ +function mysp_deactivate() { + // Flush rewrite rules + flush_rewrite_rules(); + + // Clear any scheduled events + wp_clear_scheduled_hook( 'mysp_cron_event' ); +} +register_deactivation_hook( __FILE__, 'mysp_deactivate' ); diff --git a/templates/plugin-simple/uninstall.php b/templates/plugin-simple/uninstall.php new file mode 100644 index 0000000..0f3af3c --- /dev/null +++ b/templates/plugin-simple/uninstall.php @@ -0,0 +1,56 @@ +get_col( "SELECT blog_id FROM $wpdb->blogs" ); + + foreach ( $blog_ids as $blog_id ) { + switch_to_blog( $blog_id ); + + delete_option( 'mysp_option' ); + delete_option( 'mysp_activated_time' ); + delete_transient( 'mysp_cache' ); + + restore_current_blog(); + } +} + +// Delete custom post type data (optional - consider if users want to keep content) +// Uncomment the following if you want to delete all custom post type posts on uninstall +/* +$books = get_posts( array( + 'post_type' => 'book', + 'posts_per_page' => -1, + 'post_status' => 'any', +) ); + +foreach ( $books as $book ) { + wp_delete_post( $book->ID, true ); // true = force delete (skip trash) +} +*/ + +// If you created custom database tables, drop them here +/* +global $wpdb; +$table_name = $wpdb->prefix . 'mysp_custom_table'; +$wpdb->query( "DROP TABLE IF EXISTS $table_name" ); +*/