Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -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": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -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.
|
||||||
14
assets/example-template.txt
Normal file
14
assets/example-template.txt
Normal file
@@ -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.
|
||||||
544
examples/github-updater.php
Normal file
544
examples/github-updater.php
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Example: GitHub Auto-Updates
|
||||||
|
*
|
||||||
|
* This example demonstrates how to implement automatic updates from GitHub
|
||||||
|
* using the Plugin Update Checker library by YahnisElsts.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Automatic updates from GitHub releases, tags, or branches
|
||||||
|
* - Support for public and private repositories
|
||||||
|
* - License key integration (optional)
|
||||||
|
* - Secure token storage
|
||||||
|
* - Error handling and fallbacks
|
||||||
|
*
|
||||||
|
* @package YourPlugin
|
||||||
|
* @see https://github.com/YahnisElsts/plugin-update-checker
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 1: Basic GitHub Updates (Public Repository)
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Simplest implementation for public GitHub repositories.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Include Plugin Update Checker library
|
||||||
|
require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
|
||||||
|
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
|
||||||
|
|
||||||
|
// Initialize update checker
|
||||||
|
$updateChecker = PucFactory::buildUpdateChecker(
|
||||||
|
'https://github.com/yourusername/your-plugin/',
|
||||||
|
__FILE__, // Full path to main plugin file
|
||||||
|
'your-plugin' // Plugin slug
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optional: Set branch (default: master/main)
|
||||||
|
$updateChecker->setBranch( 'main' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 2: GitHub Releases (Recommended)
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Use GitHub Releases for professional versioning with release notes.
|
||||||
|
* This downloads pre-built ZIP from releases instead of source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$updateChecker = PucFactory::buildUpdateChecker(
|
||||||
|
'https://github.com/yourusername/your-plugin/',
|
||||||
|
__FILE__,
|
||||||
|
'your-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download from releases instead of source
|
||||||
|
$updateChecker->getVcsApi()->enableReleaseAssets();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To create a release:
|
||||||
|
* 1. Update version in plugin header
|
||||||
|
* 2. Commit: git commit -m "Bump version to 1.0.1"
|
||||||
|
* 3. Tag: git tag 1.0.1 && git push origin 1.0.1
|
||||||
|
* 4. Create GitHub Release with pre-built ZIP (optional)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 3: Private Repository with Authentication
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* For private repositories, use a Personal Access Token.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$updateChecker = PucFactory::buildUpdateChecker(
|
||||||
|
'https://github.com/yourusername/private-plugin/',
|
||||||
|
__FILE__,
|
||||||
|
'private-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set authentication token
|
||||||
|
$updateChecker->setAuthentication( 'ghp_YourGitHubPersonalAccessToken' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SECURITY: Never hardcode tokens!
|
||||||
|
* Use wp-config.php constant instead:
|
||||||
|
*
|
||||||
|
* In wp-config.php:
|
||||||
|
* define( 'MY_PLUGIN_GITHUB_TOKEN', 'ghp_xxx' );
|
||||||
|
*
|
||||||
|
* In plugin:
|
||||||
|
* if ( defined( 'MY_PLUGIN_GITHUB_TOKEN' ) ) {
|
||||||
|
* $updateChecker->setAuthentication( MY_PLUGIN_GITHUB_TOKEN );
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 4: Complete Implementation with Best Practices
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Production-ready implementation with error handling, caching,
|
||||||
|
* and optional license integration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize GitHub auto-updates
|
||||||
|
*/
|
||||||
|
function yourprefix_init_github_updates() {
|
||||||
|
// Path to update checker library
|
||||||
|
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
|
||||||
|
|
||||||
|
// Check if library exists
|
||||||
|
if ( ! file_exists( $updater_path ) ) {
|
||||||
|
add_action( 'admin_notices', 'yourprefix_update_checker_missing_notice' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require $updater_path;
|
||||||
|
|
||||||
|
// Initialize update checker
|
||||||
|
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
|
||||||
|
'https://github.com/yourusername/your-plugin/',
|
||||||
|
__FILE__,
|
||||||
|
'your-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set branch
|
||||||
|
$updateChecker->setBranch( 'main' );
|
||||||
|
|
||||||
|
// Use GitHub Releases
|
||||||
|
$updateChecker->getVcsApi()->enableReleaseAssets();
|
||||||
|
|
||||||
|
// Private repo authentication (from wp-config.php)
|
||||||
|
if ( defined( 'YOURPREFIX_GITHUB_TOKEN' ) ) {
|
||||||
|
$updateChecker->setAuthentication( YOURPREFIX_GITHUB_TOKEN );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: License-based updates
|
||||||
|
$license_key = get_option( 'yourprefix_license_key' );
|
||||||
|
if ( ! empty( $license_key ) && yourprefix_validate_license( $license_key ) ) {
|
||||||
|
// Use license key as authentication token
|
||||||
|
$updateChecker->setAuthentication( $license_key );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Custom update checks
|
||||||
|
add_filter( 'puc_request_info_result-your-plugin', 'yourprefix_filter_update_checks', 10, 2 );
|
||||||
|
}
|
||||||
|
add_action( 'plugins_loaded', 'yourprefix_init_github_updates' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin notice if update checker library is missing
|
||||||
|
*/
|
||||||
|
function yourprefix_update_checker_missing_notice() {
|
||||||
|
?>
|
||||||
|
<div class="notice notice-error">
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'Your Plugin:', 'your-plugin' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'Update checker library not found. Automatic updates are disabled.', 'your-plugin' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate license key (example implementation)
|
||||||
|
*
|
||||||
|
* @param string $license_key License key to validate.
|
||||||
|
* @return bool True if valid, false otherwise.
|
||||||
|
*/
|
||||||
|
function yourprefix_validate_license( $license_key ) {
|
||||||
|
// Check cached validation result
|
||||||
|
$cached = get_transient( 'yourprefix_license_valid_' . md5( $license_key ) );
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return (bool) $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate with your license server
|
||||||
|
$response = wp_remote_post(
|
||||||
|
'https://example.com/api/validate-license',
|
||||||
|
array(
|
||||||
|
'body' => array(
|
||||||
|
'license' => sanitize_text_field( $license_key ),
|
||||||
|
'domain' => home_url(),
|
||||||
|
'product' => 'your-plugin',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||||
|
|
||||||
|
if ( ! $body || ! isset( $body->valid ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_valid = (bool) $body->valid;
|
||||||
|
|
||||||
|
// Cache for 24 hours
|
||||||
|
set_transient( 'yourprefix_license_valid_' . md5( $license_key ), $is_valid, DAY_IN_SECONDS );
|
||||||
|
|
||||||
|
return $is_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter update checks (optional)
|
||||||
|
*
|
||||||
|
* Allows custom logic before updates are offered.
|
||||||
|
*
|
||||||
|
* @param object $info Update information from GitHub.
|
||||||
|
* @param object $result Response from GitHub API.
|
||||||
|
* @return object Modified update information.
|
||||||
|
*/
|
||||||
|
function yourprefix_filter_update_checks( $info, $result ) {
|
||||||
|
// Example: Block updates if license is invalid
|
||||||
|
$license_key = get_option( 'yourprefix_license_key' );
|
||||||
|
if ( empty( $license_key ) || ! yourprefix_validate_license( $license_key ) ) {
|
||||||
|
return null; // Don't show update
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Add custom data
|
||||||
|
if ( $info ) {
|
||||||
|
$info->tested = '6.4'; // Override "Tested up to" version
|
||||||
|
}
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 5: Multiple Update Channels (Stable + Beta)
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Offer beta updates to users who opt in.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function yourprefix_init_multi_channel_updates() {
|
||||||
|
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
|
||||||
|
|
||||||
|
if ( ! file_exists( $updater_path ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require $updater_path;
|
||||||
|
|
||||||
|
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
|
||||||
|
'https://github.com/yourusername/your-plugin/',
|
||||||
|
__FILE__,
|
||||||
|
'your-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if user opted into beta updates
|
||||||
|
$beta_enabled = get_option( 'yourprefix_enable_beta_updates', false );
|
||||||
|
|
||||||
|
if ( $beta_enabled ) {
|
||||||
|
// Use beta branch
|
||||||
|
$updateChecker->setBranch( 'beta' );
|
||||||
|
} else {
|
||||||
|
// Use stable releases
|
||||||
|
$updateChecker->setBranch( 'main' );
|
||||||
|
$updateChecker->getVcsApi()->enableReleaseAssets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'plugins_loaded', 'yourprefix_init_multi_channel_updates' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings field for beta opt-in
|
||||||
|
*/
|
||||||
|
function yourprefix_add_beta_settings() {
|
||||||
|
add_settings_field(
|
||||||
|
'yourprefix_enable_beta',
|
||||||
|
__( 'Enable Beta Updates', 'your-plugin' ),
|
||||||
|
'yourprefix_render_beta_field',
|
||||||
|
'your-plugin-settings',
|
||||||
|
'yourprefix_general_section'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'admin_init', 'yourprefix_add_beta_settings' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render beta updates checkbox
|
||||||
|
*/
|
||||||
|
function yourprefix_render_beta_field() {
|
||||||
|
$enabled = get_option( 'yourprefix_enable_beta_updates', false );
|
||||||
|
?>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="yourprefix_enable_beta_updates" value="1" <?php checked( $enabled, true ); ?>>
|
||||||
|
<?php esc_html_e( 'Receive beta updates (may be unstable)', 'your-plugin' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Enable this to test new features before stable release.', 'your-plugin' ); ?>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 6: GitLab Support
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Plugin Update Checker also supports GitLab and Bitbucket.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$updateChecker = PucFactory::buildUpdateChecker(
|
||||||
|
'https://gitlab.com/yourusername/your-plugin',
|
||||||
|
__FILE__,
|
||||||
|
'your-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitLab authentication
|
||||||
|
$updateChecker->setAuthentication( 'your-gitlab-private-token' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 7: Custom JSON Update Server
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Use a custom update server with JSON endpoint.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$updateChecker = PucFactory::buildUpdateChecker(
|
||||||
|
'https://example.com/updates/your-plugin.json',
|
||||||
|
__FILE__,
|
||||||
|
'your-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON format:
|
||||||
|
* {
|
||||||
|
* "version": "1.0.1",
|
||||||
|
* "download_url": "https://example.com/downloads/your-plugin-1.0.1.zip",
|
||||||
|
* "sections": {
|
||||||
|
* "description": "Plugin description",
|
||||||
|
* "changelog": "<h4>1.0.1</h4><ul><li>Bug fixes</li></ul>"
|
||||||
|
* },
|
||||||
|
* "tested": "6.4",
|
||||||
|
* "requires": "5.9",
|
||||||
|
* "requires_php": "7.4"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 8: Logging and Debugging
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Enable logging for troubleshooting update issues.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function yourprefix_init_updates_with_logging() {
|
||||||
|
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
|
||||||
|
|
||||||
|
if ( ! file_exists( $updater_path ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require $updater_path;
|
||||||
|
|
||||||
|
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
|
||||||
|
'https://github.com/yourusername/your-plugin/',
|
||||||
|
__FILE__,
|
||||||
|
'your-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enable debug mode (logs to error_log)
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// Log all API requests
|
||||||
|
add_action(
|
||||||
|
'puc_api_request_start',
|
||||||
|
function ( $url, $args ) {
|
||||||
|
error_log( sprintf( '[Plugin Updates] Checking: %s', $url ) );
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log API responses
|
||||||
|
add_action(
|
||||||
|
'puc_api_request_end',
|
||||||
|
function ( $response, $url ) {
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
error_log( sprintf( '[Plugin Updates] Error: %s', $response->get_error_message() ) );
|
||||||
|
} else {
|
||||||
|
error_log( sprintf( '[Plugin Updates] Success: %d bytes received', strlen( wp_remote_retrieve_body( $response ) ) ) );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'plugins_loaded', 'yourprefix_init_updates_with_logging' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 9: Rate Limiting Update Checks
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Prevent excessive API calls to GitHub.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function yourprefix_init_rate_limited_updates() {
|
||||||
|
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
|
||||||
|
|
||||||
|
if ( ! file_exists( $updater_path ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require $updater_path;
|
||||||
|
|
||||||
|
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
|
||||||
|
'https://github.com/yourusername/your-plugin/',
|
||||||
|
__FILE__,
|
||||||
|
'your-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set custom check period (in hours)
|
||||||
|
$updateChecker->setCheckPeriod( 12 ); // Check every 12 hours instead of default
|
||||||
|
}
|
||||||
|
add_action( 'plugins_loaded', 'yourprefix_init_rate_limited_updates' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Example 10: Cleanup on Uninstall
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Remove update checker transients when plugin is uninstalled.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// In uninstall.php
|
||||||
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete update checker transients
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Delete all transients for this plugin
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
'%puc_update_cache_your-plugin%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
'%puc_cron_check_your-plugin%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Security Notes
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* 1. ALWAYS use HTTPS for repository URLs
|
||||||
|
* 2. NEVER hardcode authentication tokens in plugin code
|
||||||
|
* 3. Store tokens in wp-config.php or encrypt them
|
||||||
|
* 4. Implement license validation before offering updates
|
||||||
|
* 5. Use checksums to verify downloaded files (see references/github-auto-updates.md)
|
||||||
|
* 6. Rate limit update checks to avoid API throttling
|
||||||
|
* 7. Log errors for debugging but don't expose sensitive data
|
||||||
|
* 8. Clear cached update data after installation
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Installation Checklist
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* 1. Install Plugin Update Checker library:
|
||||||
|
* cd your-plugin/
|
||||||
|
* git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
|
||||||
|
*
|
||||||
|
* 2. Add initialization code (see examples above)
|
||||||
|
*
|
||||||
|
* 3. For private repos, add token to wp-config.php:
|
||||||
|
* define( 'YOUR_PLUGIN_GITHUB_TOKEN', 'ghp_xxx' );
|
||||||
|
*
|
||||||
|
* 4. Test by creating a new tag on GitHub:
|
||||||
|
* git tag 1.0.1
|
||||||
|
* git push origin 1.0.1
|
||||||
|
*
|
||||||
|
* 5. Check for updates in WordPress admin:
|
||||||
|
* Dashboard → Updates → Should show your plugin
|
||||||
|
*
|
||||||
|
* 6. (Optional) Create GitHub Release for better UX:
|
||||||
|
* - Go to GitHub → Releases → Create Release
|
||||||
|
* - Upload pre-built ZIP (without .git, tests, etc.)
|
||||||
|
* - Add release notes
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Troubleshooting
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* Updates not showing?
|
||||||
|
* 1. Check plugin version in header matches current version
|
||||||
|
* 2. Verify GitHub repository URL is correct
|
||||||
|
* 3. Ensure authentication token is valid (for private repos)
|
||||||
|
* 4. Check WordPress debug log for errors
|
||||||
|
* 5. Manually clear transients: delete_site_transient( 'update_plugins' )
|
||||||
|
* 6. Verify GitHub has releases or tags
|
||||||
|
*
|
||||||
|
* Wrong version downloaded?
|
||||||
|
* 1. Ensure you're using git tags or GitHub Releases
|
||||||
|
* 2. Check branch setting matches your repository
|
||||||
|
* 3. Verify version numbers use semantic versioning (1.0.0, not v1.0.0)
|
||||||
|
*
|
||||||
|
* Installation fails?
|
||||||
|
* 1. Verify ZIP structure includes plugin folder inside ZIP
|
||||||
|
* 2. Check file permissions on server
|
||||||
|
* 3. Ensure no syntax errors in updated files
|
||||||
|
* 4. Check WordPress debug log for specific error messages
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===================================================================
|
||||||
|
* Additional Resources
|
||||||
|
* ===================================================================
|
||||||
|
*
|
||||||
|
* - Plugin Update Checker Documentation:
|
||||||
|
* https://github.com/YahnisElsts/plugin-update-checker
|
||||||
|
*
|
||||||
|
* - Complete guide with security best practices:
|
||||||
|
* See references/github-auto-updates.md
|
||||||
|
*
|
||||||
|
* - WordPress Plugin API:
|
||||||
|
* https://developer.wordpress.org/plugins/
|
||||||
|
*
|
||||||
|
* - GitHub Personal Access Tokens:
|
||||||
|
* https://github.com/settings/tokens
|
||||||
|
*/
|
||||||
165
plugin.lock.json
Normal file
165
plugin.lock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
98
references/common-hooks.md
Normal file
98
references/common-hooks.md
Normal file
@@ -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 `<head>` section | Add meta tags, styles |
|
||||||
|
| `wp_footer` | Before `</body>` | Add scripts, analytics |
|
||||||
|
| `template_redirect` | Before template loaded | Redirects, custom templates |
|
||||||
|
| `the_content` (filter) | Post content display | Modify post content |
|
||||||
|
|
||||||
|
### AJAX Hooks
|
||||||
|
|
||||||
|
| Hook | Use For |
|
||||||
|
|------|---------|
|
||||||
|
| `wp_ajax_{action}` | Logged-in AJAX |
|
||||||
|
| `wp_ajax_nopriv_{action}` | Public AJAX |
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
| Hook | When It Fires | Use For |
|
||||||
|
|------|--------------|---------|
|
||||||
|
| `rest_api_init` | REST API init | Register REST routes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filter Hooks
|
||||||
|
|
||||||
|
### Content Filters
|
||||||
|
|
||||||
|
| Hook | What It Filters | Common Use |
|
||||||
|
|------|----------------|------------|
|
||||||
|
| `the_content` | Post content | Add/modify content |
|
||||||
|
| `the_title` | Post title | Modify titles |
|
||||||
|
| `the_excerpt` | Post excerpt | Customize excerpts |
|
||||||
|
| `comment_text` | Comment text | Modify comments |
|
||||||
|
|
||||||
|
### Query Filters
|
||||||
|
|
||||||
|
| Hook | What It Filters | Common Use |
|
||||||
|
|------|----------------|------------|
|
||||||
|
| `pre_get_posts` | Query before execution | Modify queries |
|
||||||
|
| `posts_where` | SQL WHERE clause | Custom WHERE |
|
||||||
|
| `posts_orderby` | SQL ORDER BY | Custom sorting |
|
||||||
|
|
||||||
|
### Admin Filters
|
||||||
|
|
||||||
|
| Hook | What It Filters | Common Use |
|
||||||
|
|------|----------------|------------|
|
||||||
|
| `manage_{post_type}_posts_columns` | Admin columns | Add columns |
|
||||||
|
| `admin_footer_text` | Admin footer text | Custom footer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hook Priority
|
||||||
|
|
||||||
|
Default priority is `10`. Lower numbers run first.
|
||||||
|
|
||||||
|
```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
|
||||||
26
references/example-reference.md
Normal file
26
references/example-reference.md
Normal file
@@ -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.
|
||||||
1224
references/github-auto-updates.md
Normal file
1224
references/github-auto-updates.md
Normal file
File diff suppressed because it is too large
Load Diff
527
references/security-checklist.md
Normal file
527
references/security-checklist.md
Normal file
@@ -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 '<a href="' . $url . '">Link</a>';
|
||||||
|
|
||||||
|
// ✅ CORRECT - Escaped
|
||||||
|
echo esc_html( $user_input );
|
||||||
|
echo '<a href="' . esc_url( $url ) . '">Link</a>';
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Check**:
|
||||||
|
- [ ] All variables in HTML are escaped
|
||||||
|
- [ ] All variables in attributes are escaped
|
||||||
|
- [ ] All URLs are escaped
|
||||||
|
- [ ] Correct escaping function for context
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Nonces (CSRF Protection)
|
||||||
|
|
||||||
|
### Use Nonces for All Forms and AJAX
|
||||||
|
|
||||||
|
**Form example**:
|
||||||
|
|
||||||
|
```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
|
||||||
15
scripts/example-script.sh
Executable file
15
scripts/example-script.sh
Executable file
@@ -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]
|
||||||
154
scripts/scaffold-plugin.sh
Executable file
154
scripts/scaffold-plugin.sh
Executable file
@@ -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 ""
|
||||||
337
templates/examples/ajax-handler.php
Normal file
337
templates/examples/ajax-handler.php
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Example: AJAX handlers
|
||||||
|
*
|
||||||
|
* This example shows how to:
|
||||||
|
* - Register AJAX handlers for logged-in and non-logged-in users
|
||||||
|
* - Verify nonces for CSRF protection
|
||||||
|
* - Check user capabilities
|
||||||
|
* - Sanitize input and return JSON responses
|
||||||
|
* - Enqueue scripts with localized data
|
||||||
|
*
|
||||||
|
* @package YourPlugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue AJAX script
|
||||||
|
*/
|
||||||
|
function yourprefix_enqueue_ajax_script() {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'yourprefix-ajax',
|
||||||
|
plugins_url( 'js/ajax-example.js', __FILE__ ),
|
||||||
|
array( 'jquery' ),
|
||||||
|
'1.0.0',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Localize script with AJAX URL and nonce
|
||||||
|
wp_localize_script(
|
||||||
|
'yourprefix-ajax',
|
||||||
|
'yourprefixAjax',
|
||||||
|
array(
|
||||||
|
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||||
|
'nonce' => wp_create_nonce( 'yourprefix_ajax_nonce' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'wp_enqueue_scripts', 'yourprefix_enqueue_ajax_script' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for logged-in users
|
||||||
|
*/
|
||||||
|
function yourprefix_ajax_save_data() {
|
||||||
|
// Verify nonce
|
||||||
|
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
|
||||||
|
|
||||||
|
// Check user capability
|
||||||
|
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Permission denied', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and sanitize input
|
||||||
|
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';
|
||||||
|
$email = isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '';
|
||||||
|
$age = isset( $_POST['age'] ) ? absint( $_POST['age'] ) : 0;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if ( empty( $name ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Name is required', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_email( $email ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Invalid email address', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process data (example: save to database)
|
||||||
|
$result = yourprefix_save_user_data( $name, $email, $age );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => $result->get_error_message(),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( 'Data saved successfully', 'your-plugin' ),
|
||||||
|
'data' => array(
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'age' => $age,
|
||||||
|
),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
add_action( 'wp_ajax_yourprefix_save_data', 'yourprefix_ajax_save_data' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for non-logged-in users
|
||||||
|
*/
|
||||||
|
function yourprefix_ajax_public_action() {
|
||||||
|
// Verify nonce (still required for public AJAX)
|
||||||
|
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
|
||||||
|
|
||||||
|
// Get and sanitize input
|
||||||
|
$query = isset( $_POST['query'] ) ? sanitize_text_field( $_POST['query'] ) : '';
|
||||||
|
|
||||||
|
if ( empty( $query ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Query is required', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process query (example: search posts)
|
||||||
|
$results = yourprefix_search_posts( $query );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( 'Search completed', 'your-plugin' ),
|
||||||
|
'results' => $results,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
add_action( 'wp_ajax_yourprefix_public_action', 'yourprefix_ajax_public_action' );
|
||||||
|
add_action( 'wp_ajax_nopriv_yourprefix_public_action', 'yourprefix_ajax_public_action' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for fetching posts
|
||||||
|
*/
|
||||||
|
function yourprefix_ajax_load_posts() {
|
||||||
|
// Verify nonce
|
||||||
|
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
|
||||||
|
|
||||||
|
// Get parameters
|
||||||
|
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
||||||
|
$per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;
|
||||||
|
$category = isset( $_POST['category'] ) ? absint( $_POST['category'] ) : 0;
|
||||||
|
|
||||||
|
// Query posts
|
||||||
|
$args = array(
|
||||||
|
'post_type' => 'post',
|
||||||
|
'posts_per_page' => $per_page,
|
||||||
|
'paged' => $page,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $category > 0 ) {
|
||||||
|
$args['cat'] = $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new WP_Query( $args );
|
||||||
|
|
||||||
|
$posts = array();
|
||||||
|
if ( $query->have_posts() ) {
|
||||||
|
while ( $query->have_posts() ) {
|
||||||
|
$query->the_post();
|
||||||
|
$posts[] = array(
|
||||||
|
'id' => get_the_ID(),
|
||||||
|
'title' => get_the_title(),
|
||||||
|
'excerpt' => get_the_excerpt(),
|
||||||
|
'url' => get_permalink(),
|
||||||
|
'date' => get_the_date(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
wp_reset_postdata();
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'posts' => $posts,
|
||||||
|
'total_pages' => $query->max_num_pages,
|
||||||
|
'found_posts' => $query->found_posts,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
add_action( 'wp_ajax_yourprefix_load_posts', 'yourprefix_ajax_load_posts' );
|
||||||
|
add_action( 'wp_ajax_nopriv_yourprefix_load_posts', 'yourprefix_ajax_load_posts' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for deleting item
|
||||||
|
*/
|
||||||
|
function yourprefix_ajax_delete_item() {
|
||||||
|
// Verify nonce
|
||||||
|
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
|
||||||
|
|
||||||
|
// Check user capability
|
||||||
|
if ( ! current_user_can( 'delete_posts' ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Permission denied', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get item ID
|
||||||
|
$item_id = isset( $_POST['item_id'] ) ? absint( $_POST['item_id'] ) : 0;
|
||||||
|
|
||||||
|
if ( $item_id === 0 ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Invalid item ID', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item exists
|
||||||
|
$post = get_post( $item_id );
|
||||||
|
if ( ! $post ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Item not found', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete item
|
||||||
|
$result = wp_trash_post( $item_id );
|
||||||
|
|
||||||
|
if ( ! $result ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Failed to delete item', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( 'Item deleted successfully', 'your-plugin' ),
|
||||||
|
'item_id' => $item_id,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
add_action( 'wp_ajax_yourprefix_delete_item', 'yourprefix_ajax_delete_item' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for uploading file
|
||||||
|
*/
|
||||||
|
function yourprefix_ajax_upload_file() {
|
||||||
|
// Verify nonce
|
||||||
|
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
|
||||||
|
|
||||||
|
// Check user capability
|
||||||
|
if ( ! current_user_can( 'upload_files' ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Permission denied', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file was uploaded
|
||||||
|
if ( empty( $_FILES['file'] ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'No file uploaded', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file upload
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||||
|
|
||||||
|
$file = $_FILES['file'];
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
$allowed_types = array( 'image/jpeg', 'image/png', 'image/gif' );
|
||||||
|
if ( ! in_array( $file['type'], $allowed_types, true ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'Invalid file type. Only JPG, PNG, and GIF are allowed.', 'your-plugin' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload file
|
||||||
|
$attachment_id = media_handle_upload( 'file', 0 );
|
||||||
|
|
||||||
|
if ( is_wp_error( $attachment_id ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => $attachment_id->get_error_message(),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get attachment data
|
||||||
|
$attachment_url = wp_get_attachment_url( $attachment_id );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( 'File uploaded successfully', 'your-plugin' ),
|
||||||
|
'attachment_id' => $attachment_id,
|
||||||
|
'attachment_url' => $attachment_url,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
add_action( 'wp_ajax_yourprefix_upload_file', 'yourprefix_ajax_upload_file' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function: Save user data
|
||||||
|
*
|
||||||
|
* @param string $name User name.
|
||||||
|
* @param string $email User email.
|
||||||
|
* @param int $age User age.
|
||||||
|
* @return bool|WP_Error
|
||||||
|
*/
|
||||||
|
function yourprefix_save_user_data( $name, $email, $age ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table_name = $wpdb->prefix . 'yourprefix_users';
|
||||||
|
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$table_name,
|
||||||
|
array(
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'age' => $age,
|
||||||
|
),
|
||||||
|
array( '%s', '%s', '%d' )
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( false === $result ) {
|
||||||
|
return new WP_Error( 'db_error', __( 'Database error', 'your-plugin' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function: Search posts
|
||||||
|
*
|
||||||
|
* @param string $query Search query.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function yourprefix_search_posts( $query ) {
|
||||||
|
$args = array(
|
||||||
|
'post_type' => 'post',
|
||||||
|
's' => $query,
|
||||||
|
'posts_per_page' => 10,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
);
|
||||||
|
|
||||||
|
$search_query = new WP_Query( $args );
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
if ( $search_query->have_posts() ) {
|
||||||
|
while ( $search_query->have_posts() ) {
|
||||||
|
$search_query->the_post();
|
||||||
|
$results[] = array(
|
||||||
|
'id' => get_the_ID(),
|
||||||
|
'title' => get_the_title(),
|
||||||
|
'url' => get_permalink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
wp_reset_postdata();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
274
templates/examples/custom-post-type.php
Normal file
274
templates/examples/custom-post-type.php
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Example: Register custom post type and taxonomy
|
||||||
|
*
|
||||||
|
* This example shows how to:
|
||||||
|
* - Register a custom post type
|
||||||
|
* - Register a custom taxonomy
|
||||||
|
* - Link taxonomy to post type
|
||||||
|
* - Flush rewrite rules on activation
|
||||||
|
* - Use proper labels and arguments
|
||||||
|
*
|
||||||
|
* @package YourPlugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register custom post type
|
||||||
|
*/
|
||||||
|
function yourprefix_register_book_post_type() {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _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(
|
||||||
|
'<a href="%s">%s</a>',
|
||||||
|
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 );
|
||||||
|
}
|
||||||
243
templates/examples/meta-box.php
Normal file
243
templates/examples/meta-box.php
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Example: Add a meta box to post editor
|
||||||
|
*
|
||||||
|
* This example shows how to:
|
||||||
|
* - Register a meta box
|
||||||
|
* - Render meta box fields
|
||||||
|
* - Save meta box data securely
|
||||||
|
* - Use nonces for CSRF protection
|
||||||
|
* - Sanitize input and escape output
|
||||||
|
*
|
||||||
|
* @package YourPlugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add meta box
|
||||||
|
*/
|
||||||
|
function yourprefix_add_meta_box() {
|
||||||
|
add_meta_box(
|
||||||
|
'yourprefix_details', // Meta box ID
|
||||||
|
__( 'Additional Details', 'your-plugin' ), // Title
|
||||||
|
'yourprefix_render_meta_box', // Callback function
|
||||||
|
'post', // Post type (or array of post types)
|
||||||
|
'normal', // Context (normal, side, advanced)
|
||||||
|
'high' // Priority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'add_meta_boxes', 'yourprefix_add_meta_box' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render meta box content
|
||||||
|
*
|
||||||
|
* @param WP_Post $post Post object.
|
||||||
|
*/
|
||||||
|
function yourprefix_render_meta_box( $post ) {
|
||||||
|
// Add nonce for security
|
||||||
|
wp_nonce_field( 'yourprefix_save_meta', 'yourprefix_meta_nonce' );
|
||||||
|
|
||||||
|
// Get current values
|
||||||
|
$text_value = get_post_meta( $post->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 );
|
||||||
|
|
||||||
|
?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label for="yourprefix_text">
|
||||||
|
<?php esc_html_e( 'Text Field', 'your-plugin' ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="yourprefix_text"
|
||||||
|
name="yourprefix_text"
|
||||||
|
value="<?php echo esc_attr( $text_value ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Enter some text here', 'your-plugin' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label for="yourprefix_number">
|
||||||
|
<?php esc_html_e( 'Number Field', 'your-plugin' ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="yourprefix_number"
|
||||||
|
name="yourprefix_number"
|
||||||
|
value="<?php echo esc_attr( $number_value ); ?>"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
class="small-text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label for="yourprefix_select">
|
||||||
|
<?php esc_html_e( 'Select Field', 'your-plugin' ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select id="yourprefix_select" name="yourprefix_select">
|
||||||
|
<option value=""><?php esc_html_e( '-- Select --', 'your-plugin' ); ?></option>
|
||||||
|
<option value="option1" <?php selected( $select_value, 'option1' ); ?>>
|
||||||
|
<?php esc_html_e( 'Option 1', 'your-plugin' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="option2" <?php selected( $select_value, 'option2' ); ?>>
|
||||||
|
<?php esc_html_e( 'Option 2', 'your-plugin' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="option3" <?php selected( $select_value, 'option3' ); ?>>
|
||||||
|
<?php esc_html_e( 'Option 3', 'your-plugin' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<?php esc_html_e( 'Checkbox', 'your-plugin' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="yourprefix_checkbox"
|
||||||
|
name="yourprefix_checkbox"
|
||||||
|
value="1"
|
||||||
|
<?php checked( $checkbox, '1' ); ?>
|
||||||
|
/>
|
||||||
|
<?php esc_html_e( 'Enable this option', 'your-plugin' ); ?>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save meta box data
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param WP_Post $post Post object.
|
||||||
|
*/
|
||||||
|
function yourprefix_save_meta( $post_id, $post ) {
|
||||||
|
// Verify nonce
|
||||||
|
if ( ! isset( $_POST['yourprefix_meta_nonce'] ) ||
|
||||||
|
! wp_verify_nonce( $_POST['yourprefix_meta_nonce'], 'yourprefix_save_meta' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check autosave
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user permissions
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save text field
|
||||||
|
if ( isset( $_POST['yourprefix_text'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
'_yourprefix_text',
|
||||||
|
sanitize_text_field( $_POST['yourprefix_text'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save number field
|
||||||
|
if ( isset( $_POST['yourprefix_number'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
'_yourprefix_number',
|
||||||
|
absint( $_POST['yourprefix_number'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save select field
|
||||||
|
if ( isset( $_POST['yourprefix_select'] ) ) {
|
||||||
|
$allowed_values = array( 'option1', 'option2', 'option3' );
|
||||||
|
$select_value = sanitize_text_field( $_POST['yourprefix_select'] );
|
||||||
|
|
||||||
|
// Validate against allowed values
|
||||||
|
if ( in_array( $select_value, $allowed_values, true ) ) {
|
||||||
|
update_post_meta( $post_id, '_yourprefix_select', $select_value );
|
||||||
|
} else {
|
||||||
|
delete_post_meta( $post_id, '_yourprefix_select' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save checkbox
|
||||||
|
if ( isset( $_POST['yourprefix_checkbox'] ) ) {
|
||||||
|
update_post_meta( $post_id, '_yourprefix_checkbox', '1' );
|
||||||
|
} else {
|
||||||
|
delete_post_meta( $post_id, '_yourprefix_checkbox' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'save_post', 'yourprefix_save_meta', 10, 2 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display meta box data on frontend
|
||||||
|
*
|
||||||
|
* Add this to your theme's single.php or use a filter
|
||||||
|
*/
|
||||||
|
function yourprefix_display_meta_data( $content ) {
|
||||||
|
if ( ! is_singular( 'post' ) ) {
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
$text_value = get_post_meta( $post->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 = '<div class="custom-meta-data">';
|
||||||
|
$meta_html .= '<h3>' . esc_html__( 'Additional Details', 'your-plugin' ) . '</h3>';
|
||||||
|
$meta_html .= '<ul>';
|
||||||
|
|
||||||
|
if ( $text_value ) {
|
||||||
|
$meta_html .= '<li><strong>' . esc_html__( 'Text:', 'your-plugin' ) . '</strong> ' . esc_html( $text_value ) . '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $number_value ) {
|
||||||
|
$meta_html .= '<li><strong>' . esc_html__( 'Number:', 'your-plugin' ) . '</strong> ' . esc_html( $number_value ) . '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $select_value ) {
|
||||||
|
$meta_html .= '<li><strong>' . esc_html__( 'Selection:', 'your-plugin' ) . '</strong> ' . esc_html( $select_value ) . '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $checkbox ) {
|
||||||
|
$meta_html .= '<li><strong>' . esc_html__( 'Option enabled', 'your-plugin' ) . '</strong></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta_html .= '</ul>';
|
||||||
|
$meta_html .= '</div>';
|
||||||
|
|
||||||
|
$content .= $meta_html;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
add_filter( 'the_content', 'yourprefix_display_meta_data' );
|
||||||
384
templates/examples/rest-endpoint.php
Normal file
384
templates/examples/rest-endpoint.php
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Example: REST API endpoints
|
||||||
|
*
|
||||||
|
* This example shows how to:
|
||||||
|
* - Register custom REST API routes
|
||||||
|
* - Implement GET, POST, PUT, DELETE endpoints
|
||||||
|
* - Use permission callbacks for authentication
|
||||||
|
* - Validate and sanitize request parameters
|
||||||
|
* - Return proper REST responses and errors
|
||||||
|
*
|
||||||
|
* @package YourPlugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API routes
|
||||||
|
*/
|
||||||
|
function yourprefix_register_rest_routes() {
|
||||||
|
// Namespace: yourplugin/v1
|
||||||
|
$namespace = 'yourplugin/v1';
|
||||||
|
|
||||||
|
// GET /wp-json/yourplugin/v1/items
|
||||||
|
register_rest_route(
|
||||||
|
$namespace,
|
||||||
|
'/items',
|
||||||
|
array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'yourprefix_get_items',
|
||||||
|
'permission_callback' => '__return_true', // Public endpoint
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /wp-json/yourplugin/v1/items/{id}
|
||||||
|
register_rest_route(
|
||||||
|
$namespace,
|
||||||
|
'/items/(?P<id>\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<id>\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<id>\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' );
|
||||||
308
templates/examples/settings-page.php
Normal file
308
templates/examples/settings-page.php
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Example: Settings page using WordPress Settings API
|
||||||
|
*
|
||||||
|
* This example shows how to:
|
||||||
|
* - Add a settings page to WordPress admin
|
||||||
|
* - Register settings with Settings API
|
||||||
|
* - Add settings sections and fields
|
||||||
|
* - Sanitize and validate settings
|
||||||
|
* - Use different field types
|
||||||
|
*
|
||||||
|
* @package YourPlugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add settings page to admin menu
|
||||||
|
*/
|
||||||
|
function yourprefix_add_settings_page() {
|
||||||
|
add_options_page(
|
||||||
|
__( 'Your Plugin Settings', 'your-plugin' ), // Page title
|
||||||
|
__( 'Your Plugin', 'your-plugin' ), // Menu title
|
||||||
|
'manage_options', // Capability
|
||||||
|
'your-plugin-settings', // Menu slug
|
||||||
|
'yourprefix_render_settings_page' // Callback function
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'admin_menu', 'yourprefix_add_settings_page' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register settings
|
||||||
|
*/
|
||||||
|
function yourprefix_register_settings() {
|
||||||
|
// Register setting
|
||||||
|
register_setting(
|
||||||
|
'yourprefix_settings_group', // Option group
|
||||||
|
'yourprefix_settings', // Option name
|
||||||
|
'yourprefix_sanitize_settings' // Sanitize callback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add settings section
|
||||||
|
add_settings_section(
|
||||||
|
'yourprefix_general_section', // Section ID
|
||||||
|
__( 'General Settings', 'your-plugin' ), // Section title
|
||||||
|
'yourprefix_general_section_callback', // Callback
|
||||||
|
'your-plugin-settings' // Page slug
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add settings fields
|
||||||
|
add_settings_field(
|
||||||
|
'yourprefix_text_field',
|
||||||
|
__( 'Text Field', 'your-plugin' ),
|
||||||
|
'yourprefix_text_field_callback',
|
||||||
|
'your-plugin-settings',
|
||||||
|
'yourprefix_general_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'yourprefix_number_field',
|
||||||
|
__( 'Number Field', 'your-plugin' ),
|
||||||
|
'yourprefix_number_field_callback',
|
||||||
|
'your-plugin-settings',
|
||||||
|
'yourprefix_general_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'yourprefix_checkbox_field',
|
||||||
|
__( 'Checkbox Field', 'your-plugin' ),
|
||||||
|
'yourprefix_checkbox_field_callback',
|
||||||
|
'your-plugin-settings',
|
||||||
|
'yourprefix_general_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'yourprefix_select_field',
|
||||||
|
__( 'Select Field', 'your-plugin' ),
|
||||||
|
'yourprefix_select_field_callback',
|
||||||
|
'your-plugin-settings',
|
||||||
|
'yourprefix_general_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'yourprefix_textarea_field',
|
||||||
|
__( 'Textarea Field', 'your-plugin' ),
|
||||||
|
'yourprefix_textarea_field_callback',
|
||||||
|
'your-plugin-settings',
|
||||||
|
'yourprefix_general_section'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'admin_init', 'yourprefix_register_settings' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render settings page
|
||||||
|
*/
|
||||||
|
function yourprefix_render_settings_page() {
|
||||||
|
// Check user capabilities
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message if settings were saved
|
||||||
|
if ( isset( $_GET['settings-updated'] ) ) {
|
||||||
|
add_settings_error(
|
||||||
|
'yourprefix_messages',
|
||||||
|
'yourprefix_message',
|
||||||
|
__( 'Settings Saved', 'your-plugin' ),
|
||||||
|
'updated'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error/update messages
|
||||||
|
settings_errors( 'yourprefix_messages' );
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
||||||
|
<form method="post" action="options.php">
|
||||||
|
<?php
|
||||||
|
settings_fields( 'yourprefix_settings_group' );
|
||||||
|
do_settings_sections( 'your-plugin-settings' );
|
||||||
|
submit_button( __( 'Save Settings', 'your-plugin' ) );
|
||||||
|
?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section callback
|
||||||
|
*/
|
||||||
|
function yourprefix_general_section_callback() {
|
||||||
|
echo '<p>' . esc_html__( 'Configure your plugin settings below.', 'your-plugin' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
'<input type="text" id="yourprefix_text_field" name="yourprefix_settings[text_field]" value="%s" class="regular-text" />',
|
||||||
|
esc_attr( $value )
|
||||||
|
);
|
||||||
|
echo '<p class="description">' . esc_html__( 'Enter some text here', 'your-plugin' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number field callback
|
||||||
|
*/
|
||||||
|
function yourprefix_number_field_callback() {
|
||||||
|
$settings = yourprefix_get_settings();
|
||||||
|
$value = $settings['number_field'];
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<input type="number" id="yourprefix_number_field" name="yourprefix_settings[number_field]" value="%s" min="0" max="100" class="small-text" />',
|
||||||
|
esc_attr( $value )
|
||||||
|
);
|
||||||
|
echo '<p class="description">' . esc_html__( 'Enter a number between 0 and 100', 'your-plugin' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkbox field callback
|
||||||
|
*/
|
||||||
|
function yourprefix_checkbox_field_callback() {
|
||||||
|
$settings = yourprefix_get_settings();
|
||||||
|
$value = $settings['checkbox_field'];
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<input type="checkbox" id="yourprefix_checkbox_field" name="yourprefix_settings[checkbox_field]" value="1" %s />',
|
||||||
|
checked( $value, true, false )
|
||||||
|
);
|
||||||
|
echo '<label for="yourprefix_checkbox_field">' . esc_html__( 'Enable this feature', 'your-plugin' ) . '</label>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select field callback
|
||||||
|
*/
|
||||||
|
function yourprefix_select_field_callback() {
|
||||||
|
$settings = yourprefix_get_settings();
|
||||||
|
$value = $settings['select_field'];
|
||||||
|
|
||||||
|
?>
|
||||||
|
<select id="yourprefix_select_field" name="yourprefix_settings[select_field]">
|
||||||
|
<option value=""><?php esc_html_e( '-- Select --', 'your-plugin' ); ?></option>
|
||||||
|
<option value="option1" <?php selected( $value, 'option1' ); ?>>
|
||||||
|
<?php esc_html_e( 'Option 1', 'your-plugin' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="option2" <?php selected( $value, 'option2' ); ?>>
|
||||||
|
<?php esc_html_e( 'Option 2', 'your-plugin' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="option3" <?php selected( $value, 'option3' ); ?>>
|
||||||
|
<?php esc_html_e( 'Option 3', 'your-plugin' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textarea field callback
|
||||||
|
*/
|
||||||
|
function yourprefix_textarea_field_callback() {
|
||||||
|
$settings = yourprefix_get_settings();
|
||||||
|
$value = $settings['textarea_field'];
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<textarea id="yourprefix_textarea_field" name="yourprefix_settings[textarea_field]" rows="5" cols="50" class="large-text">%s</textarea>',
|
||||||
|
esc_textarea( $value )
|
||||||
|
);
|
||||||
|
echo '<p class="description">' . esc_html__( 'Enter some longer text here', 'your-plugin' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
285
templates/plugin-oop/README.md
Normal file
285
templates/plugin-oop/README.md
Normal file
@@ -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
|
||||||
526
templates/plugin-oop/my-oop-plugin.php
Normal file
526
templates/plugin-oop/my-oop-plugin.php
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: My OOP Plugin
|
||||||
|
* Plugin URI: https://example.com/my-oop-plugin/
|
||||||
|
* Description: An object-oriented WordPress plugin using singleton pattern with security best practices.
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Requires at least: 5.9
|
||||||
|
* Requires PHP: 7.4
|
||||||
|
* Author: Your Name
|
||||||
|
* Author URI: https://example.com/
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
* Text Domain: my-oop-plugin
|
||||||
|
* Domain Path: /languages
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main plugin class using singleton pattern
|
||||||
|
*/
|
||||||
|
class My_OOP_Plugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance of the class
|
||||||
|
*
|
||||||
|
* @var My_OOP_Plugin
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin version
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const VERSION = '1.0.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin directory path
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $plugin_dir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin directory URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $plugin_url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance
|
||||||
|
*
|
||||||
|
* @return My_OOP_Plugin
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor - Initialize plugin
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->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<id>\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();
|
||||||
47
templates/plugin-oop/uninstall.php
Normal file
47
templates/plugin-oop/uninstall.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Uninstall script
|
||||||
|
*
|
||||||
|
* This file is called when the plugin is uninstalled via WordPress admin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if not called by WordPress
|
||||||
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete plugin options
|
||||||
|
delete_option( 'myop_settings' );
|
||||||
|
delete_option( 'myop_activated_time' );
|
||||||
|
|
||||||
|
// Delete transients
|
||||||
|
delete_transient( 'myop_cache' );
|
||||||
|
|
||||||
|
// For multisite
|
||||||
|
if ( is_multisite() ) {
|
||||||
|
global $wpdb;
|
||||||
|
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
|
||||||
|
|
||||||
|
foreach ( $blog_ids as $blog_id ) {
|
||||||
|
switch_to_blog( $blog_id );
|
||||||
|
|
||||||
|
delete_option( 'myop_settings' );
|
||||||
|
delete_option( 'myop_activated_time' );
|
||||||
|
delete_transient( 'myop_cache' );
|
||||||
|
|
||||||
|
restore_current_blog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete custom post type data (optional)
|
||||||
|
/*
|
||||||
|
$books = get_posts( array(
|
||||||
|
'post_type' => 'book',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'post_status' => 'any',
|
||||||
|
) );
|
||||||
|
|
||||||
|
foreach ( $books as $book ) {
|
||||||
|
wp_delete_post( $book->ID, true );
|
||||||
|
}
|
||||||
|
*/
|
||||||
87
templates/plugin-oop/views/admin-settings.php
Normal file
87
templates/plugin-oop/views/admin-settings.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin settings page template
|
||||||
|
*
|
||||||
|
* @var array $settings Current plugin settings
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
||||||
|
|
||||||
|
<?php settings_errors( 'myop_messages' ); ?>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'myop_settings_action', 'myop_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="myop_option1"><?php esc_html_e( 'Text Option', 'my-oop-plugin' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="myop_option1"
|
||||||
|
name="myop_option1"
|
||||||
|
value="<?php echo esc_attr( $settings['option1'] ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Enter some text here', 'my-oop-plugin' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="myop_option2"><?php esc_html_e( 'Number Option', 'my-oop-plugin' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="myop_option2"
|
||||||
|
name="myop_option2"
|
||||||
|
value="<?php echo esc_attr( $settings['option2'] ); ?>"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
class="small-text"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Enter a number between 0 and 100', 'my-oop-plugin' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="myop_option3"><?php esc_html_e( 'Checkbox Option', 'my-oop-plugin' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="myop_option3"
|
||||||
|
name="myop_option3"
|
||||||
|
value="1"
|
||||||
|
<?php checked( $settings['option3'], true ); ?>
|
||||||
|
/>
|
||||||
|
<?php esc_html_e( 'Enable this feature', 'my-oop-plugin' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Check to enable this feature', 'my-oop-plugin' ); ?>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php submit_button( __( 'Save Settings', 'my-oop-plugin' ), 'primary', 'myop_settings_submit' ); ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
406
templates/plugin-psr4/README.md
Normal file
406
templates/plugin-psr4/README.md
Normal file
@@ -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
|
||||||
|
<?php
|
||||||
|
namespace MyPSR4Plugin\YourNamespace;
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class YourClass {
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
// Initialize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Initialize in Plugin.php
|
||||||
|
|
||||||
|
Add to `src/Plugin.php` in the `init()` method:
|
||||||
|
|
||||||
|
```php
|
||||||
|
YourNamespace\YourClass::get_instance();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Composer Will Auto-Load
|
||||||
|
|
||||||
|
No need to manually require files! Composer's PSR-4 autoloader handles it.
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [x] ABSPATH check in every file
|
||||||
|
- [x] Namespaces prevent naming conflicts
|
||||||
|
- [x] Private constructors (singletons)
|
||||||
|
- [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] REST API permission callbacks
|
||||||
|
- [x] REST API argument validation
|
||||||
|
- [x] Conditional asset loading
|
||||||
|
- [x] Composer autoloader check
|
||||||
|
|
||||||
|
## Advantages of PSR-4 Pattern
|
||||||
|
|
||||||
|
✅ **Professional** - Industry-standard autoloading
|
||||||
|
✅ **No manual requires** - Composer handles class loading
|
||||||
|
✅ **Organized** - Classes grouped by domain/functionality
|
||||||
|
✅ **Scalable** - Easy to add new features
|
||||||
|
✅ **Testable** - Each class is independently testable
|
||||||
|
✅ **Standards** - Compatible with modern PHP tooling
|
||||||
|
✅ **Namespaces** - Prevents naming conflicts
|
||||||
|
✅ **Version control** - Clean git diffs (no huge files)
|
||||||
|
|
||||||
|
## When to Use PSR-4
|
||||||
|
|
||||||
|
**Use PSR-4 when**:
|
||||||
|
- Plugin has 20+ classes
|
||||||
|
- Multiple developers
|
||||||
|
- Need professional architecture
|
||||||
|
- Plan to add many features over time
|
||||||
|
- Want to use Composer packages
|
||||||
|
- Need unit testing
|
||||||
|
- Building a commercial plugin
|
||||||
|
|
||||||
|
**Use OOP when**:
|
||||||
|
- Plugin has 10-20 classes
|
||||||
|
- Single developer
|
||||||
|
- Don't need Composer dependencies
|
||||||
|
|
||||||
|
**Use Simple when**:
|
||||||
|
- Plugin has <10 classes
|
||||||
|
- Quick, focused functionality
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### 1. Create New Feature Class
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: Create a new AJAX handler
|
||||||
|
touch src/AJAX/BookActions.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Write Class with Namespace
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace MyPSR4Plugin\AJAX;
|
||||||
|
|
||||||
|
class BookActions {
|
||||||
|
// Your code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Initialize in Plugin.php
|
||||||
|
|
||||||
|
```php
|
||||||
|
AJAX\BookActions::get_instance();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test
|
||||||
|
|
||||||
|
No need to run `composer dump-autoload` - PSR-4 discovers classes automatically!
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
### Check Code Standards
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer phpcs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix Code Standards
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer phpcbf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Rules
|
||||||
|
|
||||||
|
Edit `composer.json` scripts:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"scripts": {
|
||||||
|
"phpcs": "phpcs --standard=WordPress --extensions=php src/",
|
||||||
|
"phpcbf": "phpcbf --standard=WordPress --extensions=php src/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Distribution & Auto-Updates
|
||||||
|
|
||||||
|
### Enabling GitHub Auto-Updates
|
||||||
|
|
||||||
|
You can provide automatic updates from GitHub without submitting to WordPress.org:
|
||||||
|
|
||||||
|
**1. Install Plugin Update Checker via Composer:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require yahnis-elsts/plugin-update-checker
|
||||||
|
```
|
||||||
|
|
||||||
|
Or as git submodule:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Create Updater class:**
|
||||||
|
|
||||||
|
Create `src/Updater.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace MyPSR4Plugin;
|
||||||
|
|
||||||
|
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
|
||||||
|
|
||||||
|
class Updater {
|
||||||
|
private static $instance = null;
|
||||||
|
private $updateChecker;
|
||||||
|
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->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
|
||||||
33
templates/plugin-psr4/composer.json
Normal file
33
templates/plugin-psr4/composer.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
templates/plugin-psr4/my-psr4-plugin.php
Normal file
58
templates/plugin-psr4/my-psr4-plugin.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: My PSR-4 Plugin
|
||||||
|
* Plugin URI: https://example.com/my-psr4-plugin/
|
||||||
|
* Description: A modern WordPress plugin using PSR-4 autoloading with Composer and namespaces.
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Requires at least: 5.9
|
||||||
|
* Requires PHP: 7.4
|
||||||
|
* Author: Your Name
|
||||||
|
* Author URI: https://example.com/
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
* Text Domain: my-psr4-plugin
|
||||||
|
* Domain Path: /languages
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
define( 'MYPP_VERSION', '1.0.0' );
|
||||||
|
define( 'MYPP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
|
define( 'MYPP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
define( 'MYPP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||||
|
|
||||||
|
// Require Composer autoloader
|
||||||
|
if ( file_exists( MYPP_PLUGIN_DIR . 'vendor/autoload.php' ) ) {
|
||||||
|
require_once MYPP_PLUGIN_DIR . 'vendor/autoload.php';
|
||||||
|
} else {
|
||||||
|
// Show admin notice if autoloader is missing
|
||||||
|
add_action( 'admin_notices', function() {
|
||||||
|
?>
|
||||||
|
<div class="notice notice-error">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: command to run */
|
||||||
|
esc_html__( 'My PSR-4 Plugin requires Composer dependencies. Please run %s in the plugin directory.', 'my-psr4-plugin' ),
|
||||||
|
'<code>composer install</code>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
} );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize plugin
|
||||||
|
\MyPSR4Plugin\Plugin::get_instance();
|
||||||
|
|
||||||
|
// Activation hook
|
||||||
|
register_activation_hook( __FILE__, array( \MyPSR4Plugin\Plugin::get_instance(), 'activate' ) );
|
||||||
|
|
||||||
|
// Deactivation hook
|
||||||
|
register_deactivation_hook( __FILE__, array( \MyPSR4Plugin\Plugin::get_instance(), 'deactivate' ) );
|
||||||
251
templates/plugin-psr4/src/API/BookEndpoints.php
Normal file
251
templates/plugin-psr4/src/API/BookEndpoints.php
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Book REST API Endpoints
|
||||||
|
*
|
||||||
|
* @package MyPSR4Plugin\API
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace MyPSR4Plugin\API;
|
||||||
|
|
||||||
|
use MyPSR4Plugin\PostTypes\Book;
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Book REST API endpoints class
|
||||||
|
*/
|
||||||
|
class BookEndpoints {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance
|
||||||
|
*
|
||||||
|
* @var BookEndpoints
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const NAMESPACE = 'mypp/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instance
|
||||||
|
*
|
||||||
|
* @return BookEndpoints
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST routes
|
||||||
|
*/
|
||||||
|
public function register_routes() {
|
||||||
|
// Get all books
|
||||||
|
register_rest_route(
|
||||||
|
self::NAMESPACE,
|
||||||
|
'/books',
|
||||||
|
array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array( $this, 'get_books' ),
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get single book
|
||||||
|
register_rest_route(
|
||||||
|
self::NAMESPACE,
|
||||||
|
'/books/(?P<id>\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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
248
templates/plugin-psr4/src/Admin/Settings.php
Normal file
248
templates/plugin-psr4/src/Admin/Settings.php
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin Settings
|
||||||
|
*
|
||||||
|
* @package MyPSR4Plugin\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace MyPSR4Plugin\Admin;
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings class
|
||||||
|
*/
|
||||||
|
class Settings {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option name
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const OPTION_NAME = 'mypp_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instance
|
||||||
|
*
|
||||||
|
* @return Settings
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
add_action( 'admin_menu', array( $this, 'add_menu_page' ) );
|
||||||
|
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add menu page
|
||||||
|
*/
|
||||||
|
public function add_menu_page() {
|
||||||
|
add_options_page(
|
||||||
|
__( 'My PSR-4 Plugin Settings', 'my-psr4-plugin' ),
|
||||||
|
__( 'PSR-4 Plugin', 'my-psr4-plugin' ),
|
||||||
|
'manage_options',
|
||||||
|
'my-psr4-plugin',
|
||||||
|
array( $this, 'render_settings_page' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register settings
|
||||||
|
*/
|
||||||
|
public function register_settings() {
|
||||||
|
register_setting(
|
||||||
|
'mypp_settings_group',
|
||||||
|
self::OPTION_NAME,
|
||||||
|
array( $this, 'sanitize_settings' )
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_section(
|
||||||
|
'mypp_general_section',
|
||||||
|
__( 'General Settings', 'my-psr4-plugin' ),
|
||||||
|
array( $this, 'render_section_description' ),
|
||||||
|
'my-psr4-plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'mypp_option1',
|
||||||
|
__( 'Text Option', 'my-psr4-plugin' ),
|
||||||
|
array( $this, 'render_text_field' ),
|
||||||
|
'my-psr4-plugin',
|
||||||
|
'mypp_general_section',
|
||||||
|
array( 'field_id' => '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;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
||||||
|
<form method="post" action="options.php">
|
||||||
|
<?php
|
||||||
|
settings_fields( 'mypp_settings_group' );
|
||||||
|
do_settings_sections( 'my-psr4-plugin' );
|
||||||
|
submit_button( __( 'Save Settings', 'my-psr4-plugin' ) );
|
||||||
|
?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render section description
|
||||||
|
*/
|
||||||
|
public function render_section_description() {
|
||||||
|
echo '<p>' . esc_html__( 'Configure your plugin settings below.', 'my-psr4-plugin' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
'<input type="text" id="mypp_%1$s" name="%2$s[%1$s]" value="%3$s" class="regular-text" />',
|
||||||
|
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(
|
||||||
|
'<input type="number" id="mypp_%1$s" name="%2$s[%1$s]" value="%3$s" min="0" max="100" class="small-text" />',
|
||||||
|
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(
|
||||||
|
'<input type="checkbox" id="mypp_%1$s" name="%2$s[%1$s]" value="1" %3$s />',
|
||||||
|
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,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
templates/plugin-psr4/src/Frontend/Assets.php
Normal file
111
templates/plugin-psr4/src/Frontend/Assets.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Frontend Assets
|
||||||
|
*
|
||||||
|
* @package MyPSR4Plugin\Frontend
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace MyPSR4Plugin\Frontend;
|
||||||
|
|
||||||
|
use MyPSR4Plugin\PostTypes\Book;
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assets class
|
||||||
|
*/
|
||||||
|
class Assets {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance
|
||||||
|
*
|
||||||
|
* @var Assets
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instance
|
||||||
|
*
|
||||||
|
* @return Assets
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||||
|
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue frontend scripts and styles
|
||||||
|
*/
|
||||||
|
public function enqueue_scripts() {
|
||||||
|
// Only load on single book pages
|
||||||
|
if ( is_singular( Book::POST_TYPE ) ) {
|
||||||
|
wp_enqueue_style(
|
||||||
|
'mypp-style',
|
||||||
|
MYPP_PLUGIN_URL . 'assets/css/style.css',
|
||||||
|
array(),
|
||||||
|
MYPP_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'mypp-script',
|
||||||
|
MYPP_PLUGIN_URL . 'assets/js/script.js',
|
||||||
|
array( 'jquery' ),
|
||||||
|
MYPP_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue admin scripts and styles
|
||||||
|
*
|
||||||
|
* @param string $hook Current admin page hook.
|
||||||
|
*/
|
||||||
|
public function enqueue_admin_scripts( $hook ) {
|
||||||
|
// Only load on book edit pages
|
||||||
|
if ( 'post.php' !== $hook && 'post-new.php' !== $hook && 'edit.php' !== $hook ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$screen = get_current_screen();
|
||||||
|
if ( $screen && Book::POST_TYPE === $screen->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' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
templates/plugin-psr4/src/Plugin.php
Normal file
130
templates/plugin-psr4/src/Plugin.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Main Plugin class
|
||||||
|
*
|
||||||
|
* @package MyPSR4Plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace MyPSR4Plugin;
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Plugin class
|
||||||
|
*/
|
||||||
|
class Plugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance of the class
|
||||||
|
*
|
||||||
|
* @var Plugin
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance
|
||||||
|
*
|
||||||
|
* @return Plugin
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
215
templates/plugin-psr4/src/PostTypes/Book.php
Normal file
215
templates/plugin-psr4/src/PostTypes/Book.php
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Book Custom Post Type
|
||||||
|
*
|
||||||
|
* @package MyPSR4Plugin\PostTypes
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace MyPSR4Plugin\PostTypes;
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Book post type class
|
||||||
|
*/
|
||||||
|
class Book {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance
|
||||||
|
*
|
||||||
|
* @var Book
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post type slug
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const POST_TYPE = 'book';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instance
|
||||||
|
*
|
||||||
|
* @return Book
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
add_action( 'init', array( $this, 'register' ) );
|
||||||
|
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
|
||||||
|
add_action( 'save_post_' . self::POST_TYPE, array( $this, 'save_meta' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register post type
|
||||||
|
*/
|
||||||
|
public function register() {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _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 );
|
||||||
|
|
||||||
|
?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th><label for="mypp_isbn"><?php esc_html_e( 'ISBN', 'my-psr4-plugin' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="mypp_isbn"
|
||||||
|
name="mypp_isbn"
|
||||||
|
value="<?php echo esc_attr( $isbn ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="mypp_author"><?php esc_html_e( 'Author', 'my-psr4-plugin' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="mypp_author"
|
||||||
|
name="mypp_author"
|
||||||
|
value="<?php echo esc_attr( $author ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="mypp_year"><?php esc_html_e( 'Publication Year', 'my-psr4-plugin' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="mypp_year"
|
||||||
|
name="mypp_year"
|
||||||
|
value="<?php echo esc_attr( $year ); ?>"
|
||||||
|
min="1000"
|
||||||
|
max="9999"
|
||||||
|
class="small-text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save meta box data
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
*/
|
||||||
|
public function save_meta( $post_id, $post ) {
|
||||||
|
// Verify nonce
|
||||||
|
if ( ! isset( $_POST['mypp_book_meta_nonce'] ) ||
|
||||||
|
! wp_verify_nonce( $_POST['mypp_book_meta_nonce'], 'mypp_save_book_meta' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check autosave
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save ISBN
|
||||||
|
if ( isset( $_POST['mypp_isbn'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
'_mypp_isbn',
|
||||||
|
sanitize_text_field( $_POST['mypp_isbn'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Author
|
||||||
|
if ( isset( $_POST['mypp_author'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
'_mypp_author',
|
||||||
|
sanitize_text_field( $_POST['mypp_author'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Year
|
||||||
|
if ( isset( $_POST['mypp_year'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
'_mypp_year',
|
||||||
|
absint( $_POST['mypp_year'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
templates/plugin-psr4/src/Taxonomies/Genre.php
Normal file
85
templates/plugin-psr4/src/Taxonomies/Genre.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Genre Taxonomy
|
||||||
|
*
|
||||||
|
* @package MyPSR4Plugin\Taxonomies
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace MyPSR4Plugin\Taxonomies;
|
||||||
|
|
||||||
|
use MyPSR4Plugin\PostTypes\Book;
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genre taxonomy class
|
||||||
|
*/
|
||||||
|
class Genre {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance
|
||||||
|
*
|
||||||
|
* @var Genre
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taxonomy slug
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const TAXONOMY = 'genre';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instance
|
||||||
|
*
|
||||||
|
* @return Genre
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
add_action( 'init', array( $this, 'register' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register taxonomy
|
||||||
|
*/
|
||||||
|
public function register() {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
47
templates/plugin-psr4/uninstall.php
Normal file
47
templates/plugin-psr4/uninstall.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Uninstall script
|
||||||
|
*
|
||||||
|
* This file is called when the plugin is uninstalled via WordPress admin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if not called by WordPress
|
||||||
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete plugin options
|
||||||
|
delete_option( 'mypp_settings' );
|
||||||
|
delete_option( 'mypp_activated_time' );
|
||||||
|
|
||||||
|
// Delete transients
|
||||||
|
delete_transient( 'mypp_cache' );
|
||||||
|
|
||||||
|
// For multisite
|
||||||
|
if ( is_multisite() ) {
|
||||||
|
global $wpdb;
|
||||||
|
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
|
||||||
|
|
||||||
|
foreach ( $blog_ids as $blog_id ) {
|
||||||
|
switch_to_blog( $blog_id );
|
||||||
|
|
||||||
|
delete_option( '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 );
|
||||||
|
}
|
||||||
|
*/
|
||||||
181
templates/plugin-simple/README.md
Normal file
181
templates/plugin-simple/README.md
Normal file
@@ -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
|
||||||
272
templates/plugin-simple/my-simple-plugin.php
Normal file
272
templates/plugin-simple/my-simple-plugin.php
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: My Simple Plugin
|
||||||
|
* Plugin URI: https://example.com/my-simple-plugin/
|
||||||
|
* Description: A simple WordPress plugin demonstrating functional programming pattern with security best practices.
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Requires at least: 5.9
|
||||||
|
* Requires PHP: 7.4
|
||||||
|
* Author: Your Name
|
||||||
|
* Author URI: https://example.com/
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
* Text Domain: my-simple-plugin
|
||||||
|
* Domain Path: /languages
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
define( 'MYSP_VERSION', '1.0.0' );
|
||||||
|
define( 'MYSP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
|
define( 'MYSP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
define( 'MYSP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the plugin
|
||||||
|
*/
|
||||||
|
function mysp_init() {
|
||||||
|
// Load text domain for translations
|
||||||
|
load_plugin_textdomain( 'my-simple-plugin', false, dirname( MYSP_PLUGIN_BASENAME ) . '/languages' );
|
||||||
|
|
||||||
|
// Register custom post type (example)
|
||||||
|
mysp_register_book_post_type();
|
||||||
|
}
|
||||||
|
add_action( 'init', 'mysp_init' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom post type (example)
|
||||||
|
*/
|
||||||
|
function mysp_register_book_post_type() {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _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
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
||||||
|
<?php settings_errors( 'mysp_messages' ); ?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'mysp_settings_action', 'mysp_settings_nonce' ); ?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="mysp_option"><?php esc_html_e( 'Example Option', 'my-simple-plugin' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="mysp_option"
|
||||||
|
name="mysp_option"
|
||||||
|
value="<?php echo esc_attr( $option_value ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
/>
|
||||||
|
<p class="description"><?php esc_html_e( 'Enter some text here', 'my-simple-plugin' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php submit_button( __( 'Save Settings', 'my-simple-plugin' ), 'primary', 'mysp_settings_submit' ); ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example AJAX handler
|
||||||
|
*/
|
||||||
|
function mysp_ajax_handler() {
|
||||||
|
// Verify nonce
|
||||||
|
check_ajax_referer( 'mysp_ajax_nonce', 'nonce' );
|
||||||
|
|
||||||
|
// Check user capability
|
||||||
|
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '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' );
|
||||||
56
templates/plugin-simple/uninstall.php
Normal file
56
templates/plugin-simple/uninstall.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Uninstall script
|
||||||
|
*
|
||||||
|
* This file is called when the plugin is uninstalled via WordPress admin.
|
||||||
|
* It should clean up all plugin data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if not called by WordPress
|
||||||
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete plugin options
|
||||||
|
delete_option( 'mysp_option' );
|
||||||
|
delete_option( 'mysp_activated_time' );
|
||||||
|
|
||||||
|
// Delete transients
|
||||||
|
delete_transient( 'mysp_cache' );
|
||||||
|
|
||||||
|
// For multisite, delete from all sites
|
||||||
|
if ( is_multisite() ) {
|
||||||
|
global $wpdb;
|
||||||
|
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
|
||||||
|
|
||||||
|
foreach ( $blog_ids as $blog_id ) {
|
||||||
|
switch_to_blog( $blog_id );
|
||||||
|
|
||||||
|
delete_option( '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" );
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user