wordpress-plugin-core
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWordPress Plugin Development (Core)
WordPress插件开发(核心)
Last Updated: 2026-01-21
Latest Versions: WordPress 6.9+ (Dec 2, 2025), PHP 8.0+ recommended, PHP 8.5 compatible
Dependencies: None (WordPress 5.9+, PHP 7.4+ minimum)
最后更新: 2026-01-21
最新版本: WordPress 6.9+(2025年12月2日),推荐PHP 8.0+,兼容PHP 8.5
依赖: 无(最低要求WordPress 5.9+、PHP 7.4+)
Quick Start
快速开始
Architecture Patterns: Simple (functions only, <5 functions) | OOP (medium plugins) | PSR-4 (modern/large, recommended 2025+)
Plugin Header (only Plugin Name required):
php
<?php
/**
* Plugin Name: My Plugin
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Text Domain: my-plugin
*/
if ( ! defined( 'ABSPATH' ) ) exit;Security Foundation (5 essentials before writing functionality):
php
// 1. Unique Prefix
define( 'MYPL_VERSION', '1.0.0' );
function mypl_init() { /* code */ }
add_action( 'init', 'mypl_init' );
// 2. ABSPATH Check (every PHP file)
if ( ! defined( 'ABSPATH' ) ) exit;
// 3. Nonces
wp_nonce_field( 'mypl_action', 'mypl_nonce' );
wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' );
// 4. Sanitize Input, Escape Output
$clean = sanitize_text_field( $_POST['input'] );
echo esc_html( $output );
// 5. Prepared Statements
global $wpdb;
$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $id ) );架构模式: 简单型(仅函数,少于5个函数)| OOP(中型插件)| PSR-4(现代/大型插件,2025+推荐)
插件头部(仅需填写Plugin Name):
php
<?php
/**
* Plugin Name: My Plugin
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Text Domain: my-plugin
*/
if ( ! defined( 'ABSPATH' ) ) exit;安全基础(编写功能前需完成的5项要点):
php
// 1. 唯一前缀
define( 'MYPL_VERSION', '1.0.0' );
function mypl_init() { /* 代码 */ }
add_action( 'init', 'mypl_init' );
// 2. ABSPATH检查(每个PHP文件都要加)
if ( ! defined( 'ABSPATH' ) ) exit;
// 3. Nonce
wp_nonce_field( 'mypl_action', 'mypl_nonce' );
wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' );
// 4. 输入清理、输出转义
$clean = sanitize_text_field( $_POST['input'] );
echo esc_html( $output );
// 5. 预处理语句
global $wpdb;
$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $id ) );Security Foundation (Detailed)
安全基础(详细说明)
Unique Prefix (4-5 chars minimum)
唯一前缀(最少4-5个字符)
Apply to: functions, classes, constants, options, transients, meta keys. Avoid: , , .
wp____php
function mypl_function() {} // ✅
class MyPL_Class {} // ✅
function init() {} // ❌ Will conflict应用于:函数、类、常量、选项、临时数据(transients)、元数据键。避免使用:、、。
wp____php
function mypl_function() {} // ✅ 正确
class MyPL_Class {} // ✅ 正确
function init() {} // ❌ 会产生冲突Capabilities Check (Not is_admin())
权限检查(不要只用is_admin())
php
// ❌ WRONG - Security hole
if ( is_admin() ) { /* delete data */ }
// ✅ CORRECT
if ( current_user_can( 'manage_options' ) ) { /* delete data */ }Common: (Admin), (Editor/Author), (Subscriber)
manage_optionsedit_postsreadphp
// ❌ 错误 - 安全漏洞
if ( is_admin() ) { /* 删除数据 */ }
// ✅ 正确
if ( current_user_can( 'manage_options' ) ) { /* 删除数据 */ }常用权限:(管理员)、(编辑/作者)、(订阅者)
manage_optionsedit_postsreadSecurity Trinity (Input → Processing → Output)
安全三原则(输入 → 处理 → 输出)
php
// Sanitize INPUT
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$html = wp_kses_post( $_POST['content'] ); // Allow safe HTML
$ids = array_map( 'absint', $_POST['ids'] );
// Validate LOGIC
if ( ! is_email( $email ) ) wp_die( 'Invalid' );
// Escape OUTPUT
echo esc_html( $name );
echo '<a href="' . esc_url( $url ) . '">';
echo '<div class="' . esc_attr( $class ) . '">';php
// 清理输入(INPUT)
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$html = wp_kses_post( $_POST['content'] ); // 允许安全的HTML
$ids = array_map( 'absint', $_POST['ids'] );
// 验证逻辑(LOGIC)
if ( ! is_email( $email ) ) wp_die( '无效邮箱' );
// 转义输出(OUTPUT)
echo esc_html( $name );
echo '<a href="' . esc_url( $url ) . '">';
echo '<div class="' . esc_attr( $class ) . '">';Nonces (CSRF Protection)
Nonce(CSRF防护)
php
// Form
<?php wp_nonce_field( 'mypl_action', 'mypl_nonce' ); ?>
if ( ! wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' ) ) wp_die( 'Failed' );
// AJAX
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
wp_localize_script( 'mypl-script', 'mypl_ajax_object', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ),
) );php
// 表单
<?php wp_nonce_field( 'mypl_action', 'mypl_nonce' ); ?>
if ( ! wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' ) ) wp_die( '验证失败' );
// AJAX
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
wp_localize_script( 'mypl-script', 'mypl_ajax_object', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ),
) );Prepared Statements
预处理语句
php
// ❌ SQL Injection
$wpdb->get_results( "SELECT * FROM table WHERE id = {$_GET['id']}" );
// ✅ Prepared (%s=String, %d=Integer, %f=Float)
$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );
// LIKE Queries
$search = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );php
// ❌ SQL注入风险
$wpdb->get_results( "SELECT * FROM table WHERE id = {$_GET['id']}" );
// ✅ 安全写法(%s=字符串,%d=整数,%f=浮点数)
$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );
// LIKE查询
$search = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );Critical Rules
关键规则
Always Do
务必遵守
✅ Use unique prefix (4-5 chars) for all global code (functions, classes, options, transients)
✅ Add ABSPATH check to every PHP file:
✅ Check capabilities () not just
✅ Verify nonces for all forms and AJAX requests
✅ Use $wpdb->prepare() for all database queries with user input
✅ Sanitize input with functions before saving
✅ Escape output with functions before displaying
✅ Flush rewrite rules on activation when registering custom post types
✅ Use uninstall.php for permanent cleanup (not deactivation hook)
✅ Follow WordPress Coding Standards (tabs for indentation, Yoda conditions)
if ( ! defined( 'ABSPATH' ) ) exit;current_user_can()is_admin()sanitize_*()esc_*()✅ 使用唯一前缀(4-5个字符)命名所有全局代码(函数、类、选项、临时数据)
✅ 为每个PHP文件添加ABSPATH检查:
✅ 检查权限(),不要只依赖
✅ 验证所有表单和AJAX请求的Nonce
✅ 所有包含用户输入的数据库查询都使用$wpdb->prepare()
✅ 保存前使用函数清理输入
✅ 显示前使用函数转义输出
✅ 注册自定义文章类型时,在激活插件时刷新重写规则
✅ 使用uninstall.php进行永久清理(不要用停用钩子)
✅ 遵循WordPress编码规范(用制表符缩进,Yoda条件判断)
if ( ! defined( 'ABSPATH' ) ) exit;current_user_can()is_admin()sanitize_*()esc_*()Never Do
切勿执行
❌ Never use extract() - Creates security vulnerabilities
❌ Never trust $_POST/$_GET without sanitization
❌ Never concatenate user input into SQL - Always use prepare()
❌ Never use alone for permission checks
❌ Never output unsanitized data - Always escape
❌ Never use generic function/class names - Always prefix
❌ Never use short PHP tags or - Use only
❌ Never delete user data on deactivation - Only on uninstall
❌ Never register uninstall hook repeatedly - Only once on activation
❌ Never use in main flow - Use uninstall.php instead
is_admin()<?<?=<?phpregister_uninstall_hook()❌ 永远不要使用extract() - 会产生安全漏洞
❌ 未经过清理不要信任$_POST/$_GET数据
❌ 永远不要将用户输入直接拼接进SQL语句 - 务必使用prepare()
❌ 永远不要仅用做权限检查
❌ 永远不要输出未清理的数据 - 务必转义
❌ 永远不要使用通用的函数/类名 - 务必添加前缀
❌ 永远不要使用短PHP标签 或 - 仅使用
❌ 永远不要在停用插件时删除用户数据 - 仅在卸载时删除
❌ 永远不要重复注册卸载钩子 - 仅在激活时注册一次
❌ 永远不要在主流程中使用 - 改用uninstall.php
is_admin()<?<?=<?phpregister_uninstall_hook()Known Issues Prevention
已知问题预防
This skill prevents 29 documented issues:
本技能可预防29个已记录的问题:
Issue #1: SQL Injection
问题#1:SQL注入
Error: Database compromised via unescaped user input
Source: https://patchstack.com/articles/sql-injection/ (15% of all vulnerabilities)
Why It Happens: Direct concatenation of user input into SQL queries
Prevention: Always use with placeholders
$wpdb->prepare()php
// VULNERABLE
$wpdb->query( "DELETE FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" );
// SECURE
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );错误:通过未转义的用户输入导致数据库被攻陷
来源:https://patchstack.com/articles/sql-injection/(占所有漏洞的15%)
原因:将用户输入直接拼接进SQL查询
预防:始终使用带占位符的
$wpdb->prepare()php
// 存在漏洞
$wpdb->query( "DELETE FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" );
// 安全写法
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );Issue #2: XSS (Cross-Site Scripting)
问题#2:XSS(跨站脚本攻击)
Error: Malicious JavaScript executed in user browsers
Source: https://patchstack.com (35% of all vulnerabilities)
Why It Happens: Outputting unsanitized user data to HTML
Prevention: Always escape output with context-appropriate function
php
// VULNERABLE
echo $_POST['name'];
echo '<div class="' . $_POST['class'] . '">';
// SECURE
echo esc_html( $_POST['name'] );
echo '<div class="' . esc_attr( $_POST['class'] ) . '">';错误:恶意JavaScript在用户浏览器中执行
来源:https://patchstack.com(占所有漏洞的35%)
原因:将未清理的用户数据输出到HTML中
预防:始终使用适合上下文的函数转义输出
php
// 存在漏洞
echo $_POST['name'];
echo '<div class="' . $_POST['class'] . '">';
// 安全写法
echo esc_html( $_POST['name'] );
echo '<div class="' . esc_attr( $_POST['class'] ) . '">';Issue #3: CSRF (Cross-Site Request Forgery)
问题#3:CSRF(跨站请求伪造)
Error: Unauthorized actions performed on behalf of users
Source: https://blog.nintechnet.com/25-wordpress-plugins-vulnerable-to-csrf-attacks/
Why It Happens: No verification that requests originated from your site
Prevention: Use nonces with and
wp_nonce_field()wp_verify_nonce()php
// VULNERABLE
if ( $_POST['action'] == 'delete' ) {
delete_user( $_POST['user_id'] );
}
// SECURE
if ( ! wp_verify_nonce( $_POST['nonce'], 'mypl_delete_user' ) ) {
wp_die( 'Security check failed' );
}
delete_user( absint( $_POST['user_id'] ) );错误:代表用户执行未授权操作
来源:https://blog.nintechnet.com/25-wordpress-plugins-vulnerable-to-csrf-attacks/
原因:未验证请求是否来自你的站点
预防:使用和处理Nonce
wp_nonce_field()wp_verify_nonce()php
// 存在漏洞
if ( $_POST['action'] == 'delete' ) {
delete_user( $_POST['user_id'] );
}
// 安全写法
if ( ! wp_verify_nonce( $_POST['nonce'], 'mypl_delete_user' ) ) {
wp_die( '安全检查失败' );
}
delete_user( absint( $_POST['user_id'] ) );Issue #4: Missing Capability Checks
问题#4:缺少权限检查
Error: Regular users can access admin functions
Source: WordPress Security Review Guidelines
Why It Happens: Using instead of
Prevention: Always check capabilities, not just admin context
is_admin()current_user_can()php
// VULNERABLE
if ( is_admin() ) {
// Any logged-in user can trigger this
}
// SECURE
if ( current_user_can( 'manage_options' ) ) {
// Only administrators can trigger this
}错误:普通用户可访问管理员功能
来源:WordPress安全审查指南
原因:使用而非
预防:始终检查权限,不要只依赖管理员上下文
is_admin()current_user_can()php
// 存在漏洞
if ( is_admin() ) {
// 任何已登录用户都可以触发
}
// 安全写法
if ( current_user_can( 'manage_options' ) ) {
// 仅管理员可以触发
}Issue #5: Direct File Access
问题#5:直接文件访问
Error: PHP files executed outside WordPress context
Source: WordPress Plugin Handbook
Why It Happens: No ABSPATH check at top of file
Prevention: Add ABSPATH check to every PHP file
php
// Add to top of EVERY PHP file
if ( ! defined( 'ABSPATH' ) ) {
exit;
}错误:PHP文件在WordPress上下文之外被执行
来源:WordPress插件手册
原因:文件顶部没有ABSPATH检查
预防:为每个PHP文件添加ABSPATH检查
php
// 添加到每个PHP文件的顶部
if ( ! defined( 'ABSPATH' ) ) {
exit;
}Issue #6: Prefix Collision
问题#6:前缀冲突
Error: Functions/classes conflict with other plugins
Source: WordPress Coding Standards
Why It Happens: Generic names without unique prefix
Prevention: Use 4-5 character prefix on ALL global code
php
// CAUSES CONFLICTS
function init() {}
class Settings {}
add_option( 'api_key', $value );
// SAFE
function mypl_init() {}
class MyPL_Settings {}
add_option( 'mypl_api_key', $value );错误:函数/类与其他插件冲突
来源:WordPress编码规范
原因:使用通用名称而未加唯一前缀
预防:为所有全局代码添加4-5个字符的唯一前缀
php
// 会产生冲突
function init() {}
class Settings {}
add_option( 'api_key', $value );
// 安全写法
function mypl_init() {}
class MyPL_Settings {}
add_option( 'mypl_api_key', $value );Issue #7: Rewrite Rules Not Flushed (and Performance)
问题#7:未刷新重写规则(及性能问题)
Error: Custom post types return 404 errors, or database overload from repeated flushing
Source: WordPress Plugin Handbook, Permalink Manager Pro
Why It Happens: Forgot to flush rewrite rules after registering CPT, OR calling flush on every page load
Prevention: Flush ONLY on activation/deactivation, NEVER on every page load
php
// ✅ CORRECT - Only flush on activation
function mypl_activate() {
mypl_register_cpt();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
// ❌ WRONG - Causes database overload on EVERY page load
add_action( 'init', 'mypl_register_cpt' );
add_action( 'init', 'flush_rewrite_rules' ); // BAD! Performance killer!
// ❌ WRONG - In functions.php
function mypl_register_cpt() {
register_post_type( 'book', ... );
flush_rewrite_rules(); // BAD! Runs every time
}User-Facing Fix: If CPT shows 404, manually flush by going to Settings → Permalinks → Save Changes.
错误:自定义文章类型返回404错误,或因重复刷新导致数据库过载
来源:WordPress插件手册、Permalink Manager Pro
原因:注册自定义文章类型后忘记刷新重写规则,或在每次页面加载时调用刷新
预防:仅在激活/停用插件时刷新,绝不要在每次页面加载时刷新
php
// ✅ 正确 - 仅在激活时刷新
function mypl_activate() {
mypl_register_cpt();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mypl_activate' );
function mypl_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'mypl_deactivate' );
// ❌ 错误 - 每次页面加载都会导致数据库过载
add_action( 'init', 'mypl_register_cpt' );
add_action( 'init', 'flush_rewrite_rules' ); // 糟糕!会拖垮性能!
// ❌ 错误 - 在functions.php中
function mypl_register_cpt() {
register_post_type( 'book', ... );
flush_rewrite_rules(); // 糟糕!每次都会执行
}面向用户的修复方法:如果自定义文章类型显示404,手动刷新:进入设置 → 固定链接 → 保存更改。
Issue #8: Transients Not Cleaned
问题#8:临时数据未清理
Error: Database accumulates expired transients
Source: WordPress Transients API Documentation
Why It Happens: No cleanup on uninstall
Prevention: Delete transients in uninstall.php
php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mypl_%'" );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mypl_%'" );错误:数据库中累积过期的临时数据
来源:WordPress Transients API文档
原因:卸载插件时未清理临时数据
预防:在uninstall.php中删除临时数据
php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mypl_%'" );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mypl_%'" );Issue #9: Scripts Loaded Everywhere
问题#9:脚本全局加载
Error: Performance degraded by unnecessary asset loading
Source: WordPress Performance Best Practices
Why It Happens: Enqueuing scripts/styles without conditional checks
Prevention: Only load assets where needed
php
// BAD - Loads on every page
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script( 'mypl-script', $url );
} );
// GOOD - Only loads on specific page
add_action( 'wp_enqueue_scripts', function() {
if ( is_page( 'my-page' ) ) {
wp_enqueue_script( 'mypl-script', $url, array( 'jquery' ), '1.0', true );
}
} );错误:不必要的资源加载导致性能下降
来源:WordPress性能最佳实践
原因:未加条件判断就加载脚本/样式
预防:仅在需要的地方加载资源
php
// 糟糕 - 在所有页面加载
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script( 'mypl-script', $url );
} );
// 良好 - 仅在特定页面加载
add_action( 'wp_enqueue_scripts', function() {
if ( is_page( 'my-page' ) ) {
wp_enqueue_script( 'mypl-script', $url, array( 'jquery' ), '1.0', true );
}
} );Issue #10: Missing Sanitization on Save
问题#10:保存时未清理数据
Error: Malicious data stored in database
Source: WordPress Data Validation
Why It Happens: Saving $_POST data without sanitization
Prevention: Always sanitize before saving
php
// VULNERABLE
update_option( 'mypl_setting', $_POST['value'] );
// SECURE
update_option( 'mypl_setting', sanitize_text_field( $_POST['value'] ) );错误:恶意数据被存储到数据库
来源:WordPress数据验证指南
原因:未清理就保存$_POST数据
预防:保存前务必清理数据
php
// 存在漏洞
update_option( 'mypl_setting', $_POST['value'] );
// 安全写法
update_option( 'mypl_setting', sanitize_text_field( $_POST['value'] ) );Issue #11: Incorrect LIKE Queries
问题#11:LIKE查询错误
Error: SQL syntax errors or injection vulnerabilities
Source: WordPress $wpdb Documentation
Why It Happens: LIKE wildcards not escaped properly
Prevention: Use
$wpdb->esc_like()php
// WRONG
$search = '%' . $term . '%';
// CORRECT
$search = '%' . $wpdb->esc_like( $term ) . '%';
$results = $wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );错误:SQL语法错误或注入漏洞
来源:WordPress $wpdb文档
原因:LIKE通配符未正确转义
预防:使用
$wpdb->esc_like()php
// 错误
$search = '%' . $term . '%';
// 正确
$search = '%' . $wpdb->esc_like( $term ) . '%';
$results = $wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );Issue #12: Using extract()
问题#12:使用extract()
Error: Variable collision and security vulnerabilities
Source: WordPress Coding Standards
Why It Happens: extract() creates variables from array keys
Prevention: Never use extract(), access array elements directly
php
// DANGEROUS
extract( $_POST );
// Now $any_array_key becomes a variable
// SAFE
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';错误:变量冲突和安全漏洞
来源:WordPress编码规范
原因:extract()会从数组键创建变量
预防:永远不要使用extract(),直接访问数组元素
php
// 危险
extract( $_POST );
// 现在任何数组键都变成了变量
// 安全写法
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';Issue #13: Missing Permission Callback in REST API
问题#13:REST API缺少权限回调
Error: Endpoints accessible to everyone, allowing unauthorized access or privilege escalation
Source: WordPress REST API Handbook, Patchstack CVE Database
Why It Happens: No specified, or missing for sensitive endpoints
Prevention: Always add permission_callback AND hide sensitive endpoints from REST index
permission_callbackshow_in_index => falseReal 2025-2026 Vulnerabilities:
- All in One SEO (3M+ sites): Missing permission check allowed contributor-level users to view global AI access token
- AI Engine Plugin (CVE-2025-11749, CVSS 9.8 Critical): Failed to set , exposed bearer token in /wp-json/ index, full admin privileges granted to unauthenticated attackers
show_in_index => false - SureTriggers: Insufficient authorization checks exploited within 4 hours of disclosure
- Worker for Elementor (CVE-2025-66144): Subscriber-level privileges could invoke restricted features
php
// ❌ VULNERABLE - Missing permission_callback (WordPress 5.5+ requires it!)
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'GET',
'callback' => 'my_callback',
) );
// ✅ SECURE - Basic protection
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'GET',
'callback' => 'my_callback',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );
// ✅ SECURE - Hide sensitive endpoints from REST index
register_rest_route( 'myplugin/v1', '/admin', array(
'methods' => 'POST',
'callback' => 'my_admin_callback',
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
'show_in_index' => false, // Don't expose in /wp-json/
) );2025-2026 Statistics: 64,782 total vulnerabilities tracked, 333 new in one week, 236 remained unpatched. REST API auth issues represent significant percentage.
错误:端点对所有人开放,允许未授权访问或权限提升
来源:WordPress REST API手册、Patchstack CVE数据库
原因:未指定,或敏感端点未设置
预防:始终添加permission_callback,并隐藏敏感端点的REST索引
permission_callbackshow_in_index => false2025-2026年真实漏洞:
- All in One SEO(300万+站点):缺少权限检查,允许贡献者级别用户查看全局AI访问令牌
- AI Engine插件(CVE-2025-11749,CVSS 9.8 严重):未设置,在/wp-json/索引中暴露Bearer令牌,未认证攻击者可获得完整管理员权限
show_in_index => false - SureTriggers:权限检查不足,在披露后4小时内被利用
- Worker for Elementor(CVE-2025-66144):订阅者级别权限可调用受限功能
php
// ❌ 存在漏洞 - 缺少permission_callback(WordPress 5.5+要求必须设置!)
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'GET',
'callback' => 'my_callback',
) );
// ✅ 安全写法 - 基础防护
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'GET',
'callback' => 'my_callback',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );
// ✅ 安全写法 - 隐藏敏感端点的REST索引
register_rest_route( 'myplugin/v1', '/admin', array(
'methods' => 'POST',
'callback' => 'my_admin_callback',
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
'show_in_index' => false, // 不要在/wp-json/中暴露
) );2025-2026年统计数据:共跟踪64,782个漏洞,一周内新增333个,236个未修复。REST API认证问题占比显著。
Issue #14: Uninstall Hook Registered Repeatedly
问题#14:重复注册卸载钩子
Error: Option written on every page load
Source: WordPress Plugin Handbook
Why It Happens: register_uninstall_hook() called in main flow
Prevention: Use uninstall.php file instead
php
// BAD - Runs on every page load
register_uninstall_hook( __FILE__, 'mypl_uninstall' );
// GOOD - Use uninstall.php file (preferred method)
// Create uninstall.php in plugin root错误:每次页面加载都会写入选项
来源:WordPress插件手册
原因:在主流程中调用register_uninstall_hook()
预防:改用uninstall.php文件
php
// 糟糕 - 每次页面加载都会执行
register_uninstall_hook( __FILE__, 'mypl_uninstall' );
// 良好 - 使用uninstall.php文件(推荐方法)
// 在插件根目录创建uninstall.phpIssue #15: Data Deleted on Deactivation
问题#15:停用插件时删除数据
Error: Users lose data when temporarily disabling plugin
Source: WordPress Plugin Development Best Practices
Why It Happens: Confusion about deactivation vs uninstall
Prevention: Only delete data in uninstall.php, never on deactivation
php
// WRONG - Deletes user data on deactivation
register_deactivation_hook( __FILE__, function() {
delete_option( 'mypl_user_settings' );
} );
// CORRECT - Only clear temporary data on deactivation
register_deactivation_hook( __FILE__, function() {
delete_transient( 'mypl_cache' );
} );
// CORRECT - Delete all data in uninstall.php错误:用户临时停用插件时丢失数据
来源:WordPress插件开发最佳实践
原因:混淆了停用和卸载的区别
预防:仅在uninstall.php中删除数据,永远不要在停用时删除
php
// 错误 - 停用时删除用户数据
register_deactivation_hook( __FILE__, function() {
delete_option( 'mypl_user_settings' );
} );
// 正确 - 仅在停用时清理临时数据
register_deactivation_hook( __FILE__, function() {
delete_transient( 'mypl_cache' );
} );
// 正确 - 在uninstall.php中删除所有数据Issue #16: Using Deprecated Functions
问题#16:使用已弃用函数
Error: Plugin breaks on WordPress updates
Source: WordPress Deprecated Functions List
Why It Happens: Using functions removed in newer WordPress versions
Prevention: Enable WP_DEBUG during development
php
// In wp-config.php (development only)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );错误:WordPress更新后插件崩溃
来源:WordPress已弃用函数列表
原因:使用了在新版本WordPress中被移除的函数
预防:开发期间启用WP_DEBUG
php
// 在wp-config.php中(仅开发环境)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );Issue #17: Text Domain Mismatch
问题#17:文本域不匹配
Error: Translations don't load
Source: WordPress Internationalization
Why It Happens: Text domain doesn't match plugin slug
Prevention: Use exact plugin slug everywhere
php
// Plugin header
// Text Domain: my-plugin
// In code - MUST MATCH EXACTLY
__( 'Text', 'my-plugin' );
_e( 'Text', 'my-plugin' );错误:翻译无法加载
来源:WordPress国际化指南
原因:文本域与插件slug不匹配
预防:所有地方都使用完全一致的插件slug
php
// 插件头部
// Text Domain: my-plugin
// 代码中 - 必须完全匹配
__( 'Text', 'my-plugin' );
_e( 'Text', 'my-plugin' );Issue #18: Missing Plugin Dependencies
问题#18:缺少插件依赖检查
Error: Fatal error when required plugin is inactive
Source: WordPress Plugin Dependencies
Why It Happens: No check for required plugins
Prevention: Check for dependencies on plugins_loaded
php
add_action( 'plugins_loaded', function() {
if ( ! class_exists( 'WooCommerce' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>My Plugin requires WooCommerce.</p></div>';
} );
return;
}
// Initialize plugin
} );错误:所需插件未激活时触发致命错误
来源:WordPress插件依赖指南
原因:未检查所需插件是否激活
预防:在plugins_loaded钩子中检查依赖
php
add_action( 'plugins_loaded', function() {
if ( ! class_exists( 'WooCommerce' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>我的插件需要WooCommerce。</p></div>';
} );
return;
}
// 初始化插件
} );Issue #19: Autosave Triggering Meta Save
问题#19:自动保存触发元数据保存
Error: Meta saved multiple times, performance issues
Source: WordPress Post Meta
Why It Happens: No autosave check in save_post hook
Prevention: Check for DOING_AUTOSAVE constant
php
add_action( 'save_post', function( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Safe to save meta
} );错误:元数据被多次保存,导致性能问题
来源:WordPress文章元数据指南
原因:save_post钩子中未检查自动保存
预防:检查DOING_AUTOSAVE常量
php
add_action( 'save_post', function( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// 可以安全保存元数据
} );Issue #20: admin-ajax.php Performance
问题#20:admin-ajax.php性能问题
Error: Slow AJAX responses
Source: https://deliciousbrains.com/comparing-wordpress-rest-api-performance-admin-ajax-php/
Why It Happens: admin-ajax.php loads entire WordPress core
Prevention: Use REST API for new projects (10x faster)
php
// OLD: admin-ajax.php (still works but slower)
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
// NEW: REST API (10x faster, recommended)
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/endpoint', array(
'methods' => 'POST',
'callback' => 'mypl_rest_handler',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );
} );错误:AJAX响应缓慢
来源:https://deliciousbrains.com/comparing-wordpress-rest-api-performance-admin-ajax-php/
原因:admin-ajax.php会加载整个WordPress核心
预防:新项目使用REST API(速度快10倍)
php
// 旧方法:admin-ajax.php(仍可使用但速度慢)
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
// 新方法:REST API(速度快10倍,推荐)
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/endpoint', array(
'methods' => 'POST',
'callback' => 'mypl_rest_handler',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
) );
} );Issue #21: Missing show_in_rest for Block Editor
问题#21:Block Editor缺少show_in_rest
Error: Custom post types show classic editor instead of Gutenberg block editor
Source: WordPress VIP Documentation, GitHub Issue #7595
Why It Happens: Forgot to set when registering custom post type
Prevention: Always include show_in_rest for CPTs that need block editor
show_in_rest => truephp
// ❌ WRONG - Block editor won't work
register_post_type( 'book', array(
'public' => true,
'supports' => array('editor'),
// Missing show_in_rest!
) );
// ✅ CORRECT
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true, // Required for block editor
'supports' => array('editor'),
) );Critical Rule: Only post types registered with are compatible with the block editor. The block editor is dependent on the WordPress REST API. For post types that are incompatible with the block editor—or have —the classic editor will load instead.
'show_in_rest' => trueshow_in_rest => false错误:自定义文章类型显示经典编辑器而非Gutenberg块编辑器
来源:WordPress VIP文档、GitHub Issue #7595
原因:注册自定义文章类型时忘记设置
预防:需要块编辑器的自定义文章类型务必包含show_in_rest
show_in_rest => truephp
// ❌ 错误 - 块编辑器无法工作
register_post_type( 'book', array(
'public' => true,
'supports' => array('editor'),
// 缺少show_in_rest!
) );
// ✅ 正确
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true, // 块编辑器必需
'supports' => array('editor'),
) );关键规则:只有注册时设置的文章类型才兼容块编辑器。块编辑器依赖WordPress REST API。对于不兼容块编辑器的文章类型——或设置的——会加载经典编辑器。
'show_in_rest' => trueshow_in_rest => falseIssue #22: wpdb::prepare() Table Name Escaping
问题#22:wpdb::prepare()表名转义错误
Error: SQL syntax error from quoted table names, or hardcoded prefix breaks on different installations
Source: WordPress Coding Standards Issue #2442
Why It Happens: Using table names as placeholders adds quotes around the table name
Prevention: Table names must NOT be in prepare() placeholders
php
// ❌ WRONG - Adds quotes around table name
$table = $wpdb->prefix . 'my_table';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM %s WHERE id = %d",
$table, $id
) );
// Result: SELECT * FROM 'wp_my_table' WHERE id = 1
// FAILS - table name is quoted
// ❌ WRONG - Hardcoded prefix
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM wp_my_table WHERE id = %d",
$id
) );
// FAILS if user changed table prefix
// ✅ CORRECT - Table name NOT in prepare()
$table = $wpdb->prefix . 'my_table';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$id
) );
// ✅ CORRECT - Using wpdb->prefix for built-in tables
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$id
) );错误:SQL语法错误或硬编码前缀在不同安装环境中失效
来源:WordPress编码规范Issue #2442
原因:将表名作为占位符会在表名周围添加引号
预防:表名不要放在prepare()的占位符中
php
// ❌ 错误 - 表名被添加引号
$table = $wpdb->prefix . 'my_table';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM %s WHERE id = %d",
$table, $id
) );
// 结果:SELECT * FROM 'wp_my_table' WHERE id = 1
// 失败 - 表名被引号包裹
// ❌ 错误 - 硬编码前缀
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM wp_my_table WHERE id = %d",
$id
) );
// 如果用户修改了表前缀则失败
// ✅ 正确 - 表名不要放在prepare()中
$table = $wpdb->prefix . 'my_table';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$id
) );
// ✅ 正确 - 内置表使用wpdb->prefix
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$id
) );Issue #23: Nonce Verification Edge Cases
问题#23:Nonce验证边缘情况
Error: Confusing user experience from nonce failures, or false sense of security
Source: MalCare: wp_verify_nonce(), Pressidium: Understanding Nonces
Why It Happens: Misunderstanding nonce behavior and limitations
Prevention: Understand nonce edge cases and always combine with capability checks
Edge Cases:
- Time-Based Return Values:
php
$result = wp_verify_nonce( $nonce, 'action' );
// Returns 1: Valid, generated 0-12 hours ago
// Returns 2: Valid, generated 12-24 hours ago
// Returns false: Invalid or expired-
Nonce Reusability: WordPress doesn't track if a nonce has been used. They can be used multiple times within the 12-24 hour window.
-
Session Invalidation: A nonce is only valid when tied to a valid session. If a user logs out, all their nonces become invalid, causing confusing UX if they had a form open.
-
Caching Problems: Cache issues can cause mismatches when caching plugins serve an older nonce.
-
NOT a Substitute for Authorization:
php
// ❌ INSUFFICIENT - Only checks origin, not permission
if ( wp_verify_nonce( $_POST['nonce'], 'delete_user' ) ) {
delete_user( $_POST['user_id'] );
}
// ✅ CORRECT - Combine with capability check
if ( wp_verify_nonce( $_POST['nonce'], 'delete_user' ) &&
current_user_can( 'delete_users' ) ) {
delete_user( absint( $_POST['user_id'] ) );
}Key Principle (2025): Nonces should never be relied on for authentication or authorization. Always assume nonces can be compromised. Protect your functions using current_user_can().
错误:Nonce失败导致用户体验混乱,或产生虚假的安全感
来源:MalCare: wp_verify_nonce()、Pressidium: Understanding Nonces
原因:误解Nonce的行为和局限性
预防:了解Nonce的边缘情况,并始终结合权限检查
边缘情况:
- 基于时间的返回值:
php
$result = wp_verify_nonce( $nonce, 'action' );
// 返回1: 有效,生成于0-12小时前
// 返回2: 有效,生成于12-24小时前
// 返回false: 无效或已过期-
Nonce可重用性: WordPress不会跟踪Nonce是否被使用过。在12-24小时的窗口期内,它们可以被多次使用。
-
会话失效: Nonce仅在绑定到有效会话时才有效。如果用户登出,他们的所有Nonce都会失效,如果用户之前打开了表单,会导致混乱的用户体验。
-
缓存问题: 缓存插件提供旧的Nonce时会导致不匹配。
-
不能替代授权:
php
// ❌ 不足 - 仅检查来源,不检查权限
if ( wp_verify_nonce( $_POST['nonce'], 'delete_user' ) ) {
delete_user( $_POST['user_id'] );
}
// ✅ 正确 - 结合权限检查
if ( wp_verify_nonce( $_POST['nonce'], 'delete_user' ) &&
current_user_can( 'delete_users' ) ) {
delete_user( absint( $_POST['user_id'] ) );
}核心原则(2025年): Nonce永远不应该被用于身份验证或授权。始终假设Nonce可能被泄露。使用current_user_can()保护你的函数。
Issue #24: Hook Priority and Argument Count
问题#24:钩子优先级和参数数量
Error: Hook callback doesn't receive expected arguments, or runs in wrong order
Source: Kinsta: WordPress Hooks Bootcamp
Why It Happens: Default is only 1 argument, priority defaults to 10
Prevention: Specify argument count and priority explicitly when needed
php
// ❌ WRONG - Only receives $post_id
add_action( 'save_post', 'my_save_function' );
function my_save_function( $post_id, $post, $update ) {
// $post and $update are NULL!
}
// ✅ CORRECT - Specify argument count
add_action( 'save_post', 'my_save_function', 10, 3 );
function my_save_function( $post_id, $post, $update ) {
// Now all 3 arguments are available
}
// Priority matters (lower number = runs earlier)
add_action( 'init', 'first_function', 5 ); // Runs first
add_action( 'init', 'second_function', 10 ); // Default priority
add_action( 'init', 'third_function', 15 ); // Runs lastBest Practices:
- Always prefix custom hook names to avoid collisions: not
do_action( 'mypl_data_processed' )do_action( 'data_processed' ) - Filters must RETURN modified data, not echo it
- Hook placement affects backwards compatibility - choose carefully
错误:钩子回调未收到预期参数,或执行顺序错误
来源:Kinsta: WordPress Hooks Bootcamp
原因:默认仅传递1个参数,优先级默认是10
预防:需要时明确指定参数数量和优先级
php
// ❌ 错误 - 仅接收$post_id
add_action( 'save_post', 'my_save_function' );
function my_save_function( $post_id, $post, $update ) {
// $post和$update为NULL!
}
// ✅ 正确 - 指定参数数量
add_action( 'save_post', 'my_save_function', 10, 3 );
function my_save_function( $post_id, $post, $update ) {
// 现在可以获取所有3个参数
}
// 优先级很重要(数字越小,执行越早)
add_action( 'init', 'first_function', 5 ); // 先执行
add_action( 'init', 'second_function', 10 ); // 默认优先级
add_action( 'init', 'third_function', 15 ); // 最后执行最佳实践:
- 自定义钩子名称务必添加前缀以避免冲突:而非
do_action( 'mypl_data_processed' )do_action( 'data_processed' ) - 过滤器必须返回修改后的数据,不能直接输出
- 钩子的位置影响向后兼容性 - 谨慎选择
Issue #25: Custom Post Type URL Conflicts
问题#25:自定义文章类型URL冲突
Error: Individual CPT posts return 404 errors despite permalinks flushed
Source: Permalink Manager Pro: URL Conflicts
Why It Happens: CPT slug matches a page slug, creating URL conflict
Prevention: Use different slug for CPT or rename the page
php
// ❌ CONFLICT - Page and CPT use same slug
// Page URL: example.com/portfolio/
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'portfolio' ),
) );
// Individual posts 404: example.com/portfolio/my-project/
// ✅ SOLUTION 1 - Use different slug for CPT
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'projects' ),
) );
// Posts: example.com/projects/my-project/
// Page: example.com/portfolio/
// ✅ SOLUTION 2 - Use hierarchical slug
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'work/portfolio' ),
) );
// Posts: example.com/work/portfolio/my-project/
// ✅ SOLUTION 3 - Rename the page slug
// Change page from /portfolio/ to /our-portfolio/错误:尽管刷新了固定链接,单个自定义文章类型文章仍返回404错误
来源:Permalink Manager Pro: URL Conflicts
原因:自定义文章类型的slug与页面slug相同,导致URL冲突
预防:为自定义文章类型使用不同的slug,或重命名页面
php
// ❌ 冲突 - 页面和自定义文章类型使用相同slug
// 页面URL: example.com/portfolio/
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'portfolio' ),
) );
// 单个文章404: example.com/portfolio/my-project/
// ✅ 解决方案1 - 为自定义文章类型使用不同slug
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'projects' ),
) );
// 文章: example.com/projects/my-project/
// 页面: example.com/portfolio/
// ✅ 解决方案2 - 使用分层slug
register_post_type( 'portfolio', array(
'rewrite' => array( 'slug' => 'work/portfolio' ),
) );
// 文章: example.com/work/portfolio/my-project/
// ✅ 解决方案3 - 重命名页面slug
// 将页面从/portfolio/改为/our-portfolio/Issue #26: WordPress 6.8 bcrypt Password Hashing Migration
问题#26:WordPress 6.8 bcrypt密码哈希迁移
Error: Custom password hash handling breaks after WordPress 6.8 upgrade
Source: WordPress Core Make, GitHub Issue #21022
Why It Happens: WordPress 6.8+ switched from phpass to bcrypt password hashing
Prevention: Use WordPress password functions, don't handle hashes directly
What Changed (WordPress 6.8, April 2025):
- Default password hashing algorithm changed from phpass to bcrypt
- New hash prefix: (SHA-384 pre-hashed bcrypt)
$wp$2y$ - Existing passwords automatically rehashed on next login
- Popular bcrypt plugins (roots/wp-password-bcrypt) now redundant
php
// ✅ SAFE - These functions continue to work without changes
wp_hash_password( $password );
wp_check_password( $password, $hash );
// ⚠️ NEEDS UPDATE - Direct phpass hash handling
if ( strpos( $hash, '$P$' ) === 0 ) {
// Custom phpass logic - needs update for bcrypt
}
// ✅ NEW - Detect hash type
if ( strpos( $hash, '$wp$2y$' ) === 0 ) {
// bcrypt hash (WordPress 6.8+)
} elseif ( strpos( $hash, '$P$' ) === 0 ) {
// phpass hash (WordPress <6.8)
}Action Required:
- Review plugins that directly handle password hashes
- Remove bcrypt plugins when upgrading to 6.8+
- No action needed for standard wp_hash_password/wp_check_password usage
错误:WordPress 6.8升级后自定义密码哈希处理失效
来源:WordPress Core Make、GitHub Issue #21022
原因:WordPress 6.8+ 从phpass切换为bcrypt密码哈希
预防:使用WordPress密码函数,不要直接处理哈希
变更内容(WordPress 6.8,2025年4月):
- 默认密码哈希算法从phpass改为bcrypt
- 新哈希前缀:(SHA-384预哈希bcrypt)
$wp$2y$ - 现有密码会在下次登录时自动重新哈希
- 流行的bcrypt插件(roots/wp-password-bcrypt)现在已多余
php
// ✅ 安全 - 这些函数无需修改即可继续工作
wp_hash_password( $password );
wp_check_password( $password, $hash );
// ⚠️ 需要更新 - 直接处理phpass哈希
if ( strpos( $hash, '$P$' ) === 0 ) {
// 自定义phpass逻辑 - 需要更新以支持bcrypt
}
// ✅ 新方法 - 检测哈希类型
if ( strpos( $hash, '$wp$2y$' ) === 0 ) {
// bcrypt哈希(WordPress 6.8+)
} elseif ( strpos( $hash, '$P$' ) === 0 ) {
// phpass哈希(WordPress <6.8)
}需要执行的操作:
- 检查直接处理密码哈希的插件
- 升级到6.8+时移除bcrypt插件
- 使用标准wp_hash_password/wp_check_password无需操作
Issue #27: WordPress 6.9 WP_Dependencies Deprecation
问题#27:WordPress 6.9 WP_Dependencies弃用
Error: "Deprecated: Function WP_Dependencies->add_data() was called with an argument that is deprecated"
Source: WordPress 6.9 Documentation, WordPress Support Forum
Why It Happens: WordPress 6.9 (Dec 2, 2025) deprecated WP_Dependencies object methods
Prevention: Test plugins with WP_DEBUG enabled on WordPress 6.9, replace deprecated methods
Affected Plugins (confirmed):
- WooCommerce (fixed in 10.4.2)
- Yoast SEO (fixed in 26.6)
- Elementor (requires 3.24+)
Breaking Changes: WordPress 6.9 removed or modified several deprecated functions that older themes and plugins relied on, breaking custom menu walkers, classic widgets, media modals, and customizer features.
Action Required:
- Test plugins with WP_DEBUG enabled on WordPress 6.9
- Replace deprecated WP_Dependencies methods
- Check for deprecation notices in debug.log
- While top 1,000 plugins patched within hours, unmaintained plugins often lag behind
错误:"Deprecated: Function WP_Dependencies->add_data() was called with an argument that is deprecated"
来源:WordPress 6.9文档、WordPress支持论坛
原因:WordPress 6.9(2025年12月2日)弃用了WP_Dependencies对象方法
预防:在WordPress 6.9上启用WP_DEBUG测试插件,替换已弃用方法
受影响插件(已确认):
- WooCommerce(10.4.2版本修复)
- Yoast SEO(26.6版本修复)
- Elementor(需要3.24+版本)
破坏性变更:WordPress 6.9移除或修改了多个旧主题和插件依赖的已弃用函数,导致自定义菜单遍历器、经典小工具、媒体模态框和自定义器功能失效。
需要执行的操作:
- 在WordPress 6.9上启用WP_DEBUG测试插件
- 替换已弃用的WP_Dependencies方法
- 检查debug.log中的弃用通知
- 尽管前1000个插件在数小时内修复,但无人维护的插件往往滞后
Issue #28: Translation Loading Changes in WordPress 6.7
问题#28:WordPress 6.7翻译加载变更
Error: Translations don't load or debug notices appear
Source: WooCommerce Developer Blog, WordPress 6.7 Field Guide
Why It Happens: WordPress 6.7+ changed when/how translations load
Prevention: Load translations after 'init' priority 10, ensure text domain matches plugin slug
php
// ❌ WRONG - Loading too early
add_action( 'init', 'load_plugin_textdomain' );
// ✅ CORRECT - Load after 'init' priority 10
add_action( 'init', 'load_plugin_textdomain', 11 );
// Ensure text domain matches plugin slug EXACTLY
// Plugin header: Text Domain: my-plugin
__( 'Text', 'my-plugin' ); // Must match exactlyAction Required:
- Review when load_plugin_textdomain() is called
- Ensure text domain matches plugin slug exactly
- Test with WP_DEBUG enabled
错误:翻译无法加载或出现调试通知
来源:WooCommerce开发者博客、WordPress 6.7指南
原因:WordPress 6.7+ 变更了翻译加载的时间和方式
预防:在'init'优先级10之后加载翻译,确保文本域与插件slug匹配
php
// ❌ 错误 - 加载过早
add_action( 'init', 'load_plugin_textdomain' );
// ✅ 正确 - 在'init'优先级10之后加载
add_action( 'init', 'load_plugin_textdomain', 11 );
// 确保文本域与插件slug完全匹配
// 插件头部: Text Domain: my-plugin
__( 'Text', 'my-plugin' ); // 必须完全匹配需要执行的操作:
- 检查load_plugin_textdomain()的调用时机
- 确保文本域与插件slug完全匹配
- 启用WP_DEBUG测试
Issue #29: wpdb::prepare() Missing Placeholders Error
问题#29:wpdb::prepare()缺少占位符错误
Error: "The query argument of wpdb::prepare() must have a placeholder"
Source: WordPress $wpdb Documentation, SitePoint: Working with Databases
Why It Happens: Using prepare() without any placeholders
Prevention: Don't use prepare() if no dynamic data
php
// ❌ WRONG
$wpdb->prepare( "SELECT * FROM {$wpdb->posts}" );
// Error: The query argument of wpdb::prepare() must have a placeholder
// ✅ CORRECT - Don't use prepare() if no dynamic data
$wpdb->get_results( "SELECT * FROM {$wpdb->posts}" );
// ✅ CORRECT - Use prepare() for dynamic data
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$post_id
) );Additional wpdb::prepare() Mistakes:
- Percentage Sign Handling:
php
// ❌ WRONG
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE '%test%'" );
// ✅ CORRECT
$search = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
$search
) );- Mixing Argument Formats:
php
// ❌ WRONG - Can't mix individual args and array
$wpdb->prepare( "... WHERE id = %d AND name = %s", $id, array( $name ) );
// ✅ CORRECT - Pick one format
$wpdb->prepare( "... WHERE id = %d AND name = %s", $id, $name );
// OR
$wpdb->prepare( "... WHERE id = %d AND name = %s", array( $id, $name ) );错误:"The query argument of wpdb::prepare() must have a placeholder"
来源:WordPress $wpdb文档、SitePoint: Working with Databases
原因:使用prepare()但未添加任何占位符
预防:如果没有动态数据,不要使用prepare()
php
// ❌ 错误
$wpdb->prepare( "SELECT * FROM {$wpdb->posts}" );
// 错误: The query argument of wpdb::prepare() must have a placeholder
// ✅ 正确 - 没有动态数据不要使用prepare()
$wpdb->get_results( "SELECT * FROM {$wpdb->posts}" );
// ✅ 正确 - 动态数据使用prepare()
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$post_id
) );其他wpdb::prepare()错误:
- 百分号处理:
php
// ❌ 错误
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE '%test%'" );
// ✅ 正确
$search = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
$search
) );- 混合参数格式:
php
// ❌ 错误 - 不能混合单个参数和数组
$wpdb->prepare( "... WHERE id = %d AND name = %s", $id, array( $name ) );
// ✅ 正确 - 选择一种格式
$wpdb->prepare( "... WHERE id = %d AND name = %s", $id, $name );
// 或者
$wpdb->prepare( "... WHERE id = %d AND name = %s", array( $id, $name ) );Plugin Architecture Patterns
插件架构模式
Simple (Functions Only)
简单型(仅函数)
Small plugins (<5 functions):
php
function mypl_init() { /* code */ }
add_action( 'init', 'mypl_init' );小型插件(<5个函数):
php
function mypl_init() { /* 代码 */ }
add_action( 'init', 'mypl_init' );OOP (Singleton)
OOP(单例模式)
Medium plugins:
php
class MyPL_Plugin {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) self::$instance = new self();
return self::$instance;
}
private function __construct() {
add_action( 'init', array( $this, 'init' ) );
}
}
MyPL_Plugin::get_instance();中型插件:
php
class MyPL_Plugin {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) self::$instance = new self();
return self::$instance;
}
private function __construct() {
add_action( 'init', array( $this, 'init' ) );
}
}
MyPL_Plugin::get_instance();PSR-4 (Modern, Recommended 2025+)
PSR-4(现代型,2025+推荐)
Large/team plugins:
my-plugin/
├── my-plugin.php
├── composer.json → "psr-4": { "MyPlugin\\": "src/" }
└── src/Admin.php
// my-plugin.php
require_once __DIR__ . '/vendor/autoload.php';
use MyPlugin\Admin;
new Admin();大型/团队开发插件:
my-plugin/
├── my-plugin.php
├── composer.json → "psr-4": { "MyPlugin\\": "src/" }
└── src/Admin.php
// my-plugin.php
require_once __DIR__ . '/vendor/autoload.php';
use MyPlugin\Admin;
new Admin();Common Patterns
常见模式
Custom Post Types (CRITICAL: Flush rewrite rules on activation, show_in_rest for block editor):
php
// show_in_rest => true REQUIRED for Gutenberg block editor
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true, // Without this, block editor won't work!
'supports' => array( 'editor', 'title' ),
) );
register_activation_hook( __FILE__, function() {
mypl_register_cpt();
flush_rewrite_rules(); // NEVER call on every page load
} );Custom Taxonomies:
php
register_taxonomy( 'genre', 'book', array( 'hierarchical' => true, 'show_in_rest' => true ) );Meta Boxes:
php
add_meta_box( 'book_details', 'Book Details', 'mypl_meta_box_html', 'book' );
// Save: Check nonce, DOING_AUTOSAVE, current_user_can('edit_post')
update_post_meta( $post_id, '_book_isbn', sanitize_text_field( $_POST['book_isbn'] ) );Settings API:
php
register_setting( 'mypl_options', 'mypl_api_key', array( 'sanitize_callback' => 'sanitize_text_field' ) );
add_settings_section( 'mypl_section', 'API Settings', 'callback', 'my-plugin' );
add_settings_field( 'mypl_api_key', 'API Key', 'field_callback', 'my-plugin', 'mypl_section' );REST API (10x faster than admin-ajax.php):
php
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'POST',
'callback' => 'mypl_rest_callback',
'permission_callback' => fn() => current_user_can( 'edit_posts' ),
) );AJAX (Legacy, use REST API for new projects):
php
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
wp_send_json_success( array( 'message' => 'Success' ) );Custom Tables:
php
global $wpdb;
$sql = "CREATE TABLE {$wpdb->prefix}mypl_data (id bigint AUTO_INCREMENT PRIMARY KEY, ...)";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );Transients (Caching):
php
$data = get_transient( 'mypl_data' );
if ( false === $data ) {
$data = expensive_operation();
set_transient( 'mypl_data', $data, 12 * HOUR_IN_SECONDS );
}自定义文章类型(关键:激活时刷新重写规则,block editor需要show_in_rest):
php
// show_in_rest => true 是Gutenberg block editor必需的
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true, // 没有这个,block editor无法工作!
'supports' => array( 'editor', 'title' ),
) );
register_activation_hook( __FILE__, function() {
mypl_register_cpt();
flush_rewrite_rules(); // 永远不要在每次页面加载时调用
} );自定义分类法:
php
register_taxonomy( 'genre', 'book', array( 'hierarchical' => true, 'show_in_rest' => true ) );元数据框:
php
add_meta_box( 'book_details', '书籍详情', 'mypl_meta_box_html', 'book' );
// 保存: 检查Nonce、DOING_AUTOSAVE、current_user_can('edit_post')
update_post_meta( $post_id, '_book_isbn', sanitize_text_field( $_POST['book_isbn'] ) );Settings API:
php
register_setting( 'mypl_options', 'mypl_api_key', array( 'sanitize_callback' => 'sanitize_text_field' ) );
add_settings_section( 'mypl_section', 'API设置', 'callback', 'my-plugin' );
add_settings_field( 'mypl_api_key', 'API密钥', 'field_callback', 'my-plugin', 'mypl_section' );REST API(比admin-ajax.php快10倍):
php
register_rest_route( 'myplugin/v1', '/data', array(
'methods' => 'POST',
'callback' => 'mypl_rest_callback',
'permission_callback' => fn() => current_user_can( 'edit_posts' ),
) );AJAX(遗留方法,新项目使用REST API):
php
add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
wp_send_json_success( array( 'message' => '成功' ) );自定义表:
php
global $wpdb;
$sql = "CREATE TABLE {$wpdb->prefix}mypl_data (id bigint AUTO_INCREMENT PRIMARY KEY, ...)";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );临时数据(Transients)(缓存):
php
$data = get_transient( 'mypl_data' );
if ( false === $data ) {
$data = expensive_operation();
set_transient( 'mypl_data', $data, 12 * HOUR_IN_SECONDS );
}Bundled Resources
附带资源
Templates: , , , , , , ,
plugin-simple/plugin-oop/plugin-psr4/examples/meta-box.phpexamples/settings-page.phpexamples/custom-post-type.phpexamples/rest-endpoint.phpexamples/ajax-handler.phpScripts: , ,
scaffold-plugin.shcheck-security.shvalidate-headers.shReferences: , , , ,
security-checklist.mdhooks-reference.mdsanitization-guide.mdwpdb-patterns.mdcommon-errors.md模板: , , , , , , ,
plugin-simple/plugin-oop/plugin-psr4/examples/meta-box.phpexamples/settings-page.phpexamples/custom-post-type.phpexamples/rest-endpoint.phpexamples/ajax-handler.php脚本: , ,
scaffold-plugin.shcheck-security.shvalidate-headers.sh参考文档: , , , ,
security-checklist.mdhooks-reference.mdsanitization-guide.mdwpdb-patterns.mdcommon-errors.mdAdvanced Topics
高级主题
i18n (Internationalization):
php
load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
__( 'Text', 'my-plugin' ); // Return translated
_e( 'Text', 'my-plugin' ); // Echo translated
esc_html__( 'Text', 'my-plugin' ); // Translate + escapeWP-CLI:
php
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'mypl', 'MyPL_CLI_Command' );
}Cron Events:
php
register_activation_hook( __FILE__, fn() => wp_schedule_event( time(), 'daily', 'mypl_daily_task' ) );
register_deactivation_hook( __FILE__, fn() => wp_clear_scheduled_hook( 'mypl_daily_task' ) );
add_action( 'mypl_daily_task', 'mypl_do_daily_task' );Plugin Dependencies:
php
if ( ! class_exists( 'WooCommerce' ) ) {
deactivate_plugins( plugin_basename( __FILE__ ) );
add_action( 'admin_notices', fn() => echo '<div class="error"><p>Requires WooCommerce</p></div>' );
}国际化(i18n):
php
load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
__( 'Text', 'my-plugin' ); // 返回翻译后的文本
_e( 'Text', 'my-plugin' ); // 输出翻译后的文本
esc_html__( 'Text', 'my-plugin' ); // 翻译 + 转义WP-CLI:
php
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'mypl', 'MyPL_CLI_Command' );
}定时事件(Cron Events):
php
register_activation_hook( __FILE__, fn() => wp_schedule_event( time(), 'daily', 'mypl_daily_task' ) );
register_deactivation_hook( __FILE__, fn() => wp_clear_scheduled_hook( 'mypl_daily_task' ) );
add_action( 'mypl_daily_task', 'mypl_do_daily_task' );插件依赖:
php
if ( ! class_exists( 'WooCommerce' ) ) {
deactivate_plugins( plugin_basename( __FILE__ ) );
add_action( 'admin_notices', fn() => echo '<div class="error"><p>需要WooCommerce</p></div>' );
}Distribution & Auto-Updates
分发与自动更新
GitHub Auto-Updates (Plugin Update Checker by YahnisElsts):
php
// 1. Install: git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
// 2. Add to main plugin file
require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin-slug'
);
$updateChecker->getVcsApi()->enableReleaseAssets(); // Use GitHub Releases
// Private repos: Define token in wp-config.php
if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) {
$updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN );
}Deployment:
bash
git tag 1.0.1 && git push origin main && git push origin 1.0.1GitHub自动更新(YahnisElsts的Plugin Update Checker):
php
// 1. 安装: git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
// 2. 添加到主插件文件
require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin-slug'
);
$updateChecker->getVcsApi()->enableReleaseAssets(); // 使用GitHub Releases
// 私有仓库: 在wp-config.php中定义token
if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) {
$updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN );
}部署:
bash
git tag 1.0.1 && git push origin main && git push origin 1.0.1Create GitHub Release with ZIP (exclude .git, tests)
创建GitHub Release并上传ZIP(排除.git、tests)
**Alternatives**: Git Updater (no coding), Custom Update Server (full control), Freemius (commercial)
**Security**: Use HTTPS, never hardcode tokens, validate licenses, rate limit update checks
**CRITICAL**: ZIP must contain plugin folder: `plugin.zip/my-plugin/my-plugin.php`
**Resources**: See `references/github-auto-updates.md`, `examples/github-updater.php`
---
**替代方案**: Git Updater(无需编码)、自定义更新服务器(完全控制)、Freemius(商业版)
**安全**: 使用HTTPS,永远不要硬编码token,验证许可证,限制更新检查频率
**关键**: ZIP必须包含插件文件夹: `plugin.zip/my-plugin/my-plugin.php`
**资源**: 参见`references/github-auto-updates.md`, `examples/github-updater.php`
---Dependencies
依赖
Required:
- WordPress 5.9+ (recommend 6.7+)
- PHP 7.4+ (recommend 8.0+)
Optional:
- Composer 2.0+ - For PSR-4 autoloading
- WP-CLI 2.0+ - For command-line plugin management
- Query Monitor - For debugging and performance analysis
必需:
- WordPress 5.9+(推荐6.7+)
- PHP 7.4+(推荐8.0+)
可选:
- Composer 2.0+ - 用于PSR-4自动加载
- WP-CLI 2.0+ - 用于命令行插件管理
- Query Monitor - 用于调试和性能分析
Official Documentation
官方文档
- WordPress Plugin Handbook: https://developer.wordpress.org/plugins/
- WordPress Coding Standards: https://developer.wordpress.org/coding-standards/
- WordPress REST API: https://developer.wordpress.org/rest-api/
- WordPress Database Class ($wpdb): https://developer.wordpress.org/reference/classes/wpdb/
- WordPress Security: https://developer.wordpress.org/apis/security/
- Settings API: https://developer.wordpress.org/plugins/settings/settings-api/
- Custom Post Types: https://developer.wordpress.org/plugins/post-types/
- Transients API: https://developer.wordpress.org/apis/transients/
- Context7 Library ID: /websites/developer_wordpress
- WordPress插件手册: https://developer.wordpress.org/plugins/
- WordPress编码规范: https://developer.wordpress.org/coding-standards/
- WordPress REST API: https://developer.wordpress.org/rest-api/
- WordPress数据库类($wpdb): https://developer.wordpress.org/reference/classes/wpdb/
- WordPress安全: https://developer.wordpress.org/apis/security/
- Settings API: https://developer.wordpress.org/plugins/settings/settings-api/
- 自定义文章类型: https://developer.wordpress.org/plugins/post-types/
- Transients API: https://developer.wordpress.org/apis/transients/
- Context7库ID: /websites/developer_wordpress
Troubleshooting
故障排除
Fatal Error: Enable WP_DEBUG, check wp-content/debug.log, verify prefixed names, check dependencies
404 on CPT: Flush rewrite rules via Settings → Permalinks → Save
Nonce Fails: Check nonce name/action match, verify not expired (24h default)
AJAX Returns 0/-1: Verify action name matches , check nonce sent/verified
wp_ajax_{action}HTML Stripped: Use not for safe HTML
wp_kses_post()sanitize_text_field()Query Fails: Use , check , verify syntax
$wpdb->prepare()$wpdb->prefix致命错误: 启用WP_DEBUG,检查wp-content/debug.log,验证带前缀的名称,检查依赖
自定义文章类型404: 通过设置 → 固定链接 → 保存来刷新重写规则
Nonce验证失败: 检查Nonce名称/动作是否匹配,验证是否过期(默认24小时)
AJAX返回0/-1: 验证动作名称与匹配,检查Nonce是否发送/验证
wp_ajax_{action}HTML被剥离: 对于安全HTML,使用而非
wp_kses_post()sanitize_text_field()查询失败: 使用,检查,验证语法
$wpdb->prepare()$wpdb->prefixComplete Setup Checklist
完整设置检查清单
Use this checklist to verify your plugin:
- Plugin header complete with all fields
- ABSPATH check at top of every PHP file
- All functions/classes use unique prefix
- All forms have nonce verification
- All user input is sanitized
- All output is escaped
- All database queries use $wpdb->prepare()
- Capability checks (not just is_admin())
- Custom post types flush rewrite rules on activation
- Deactivation hook only clears temporary data
- uninstall.php handles permanent cleanup
- Text domain matches plugin slug
- Scripts/styles only load where needed
- WP_DEBUG enabled during development
- Tested with Query Monitor for performance
- No deprecated function warnings
- Works with latest WordPress version
Questions? Issues?
- Check for extended troubleshooting
references/common-errors.md - Verify all steps in the security foundation
- Check official docs: https://developer.wordpress.org/plugins/
- Enable WP_DEBUG and check debug.log
- Use Query Monitor plugin to debug hooks and queries
Last verified: 2026-01-21 | Skill version: 2.0.0 | Changes: Added 9 new issues from WordPress 6.7-6.9 research (bcrypt migration, WP_Dependencies deprecation, translation loading, REST API CVEs 2025-2026, wpdb::prepare() edge cases, nonce limitations, hook gotchas, CPT URL conflicts). Updated from 20 to 29 documented errors prevented. Error count: 20 → 29. Version: 1.x → 2.0.0 (major version due to significant WordPress version-specific content additions).
使用此清单验证你的插件:
- 插件头部填写完整所有字段
- 每个PHP文件顶部都有ABSPATH检查
- 所有函数/类使用唯一前缀
- 所有表单都有Nonce验证
- 所有用户输入都经过清理
- 所有输出都经过转义
- 所有数据库查询都使用$wpdb->prepare()
- 权限检查(不只是is_admin())
- 自定义文章类型在激活时刷新重写规则
- 停用钩子仅清理临时数据
- uninstall.php处理永久清理
- 文本域与插件slug匹配
- 脚本/样式仅在需要的地方加载
- 开发期间启用WP_DEBUG
- 使用Query Monitor测试性能
- 没有已弃用函数警告
- 与最新WordPress版本兼容
有疑问?有问题?
- 查看获取扩展故障排除指南
references/common-errors.md - 验证安全基础中的所有步骤
- 查看官方文档: https://developer.wordpress.org/plugins/
- 启用WP_DEBUG并检查debug.log
- 使用Query Monitor插件调试钩子和查询
最后验证: 2026-01-21 | 技能版本: 2.0.0 | 变更: 添加了WordPress 6.7-6.9研究中的9个新问题(bcrypt迁移、WP_Dependencies弃用、翻译加载、2025-2026年REST API CVE、wpdb::prepare()边缘情况、Nonce限制、钩子陷阱、自定义文章类型URL冲突)。已预防的错误数量从20个更新到29个。版本从1.x升级到2.0.0(由于添加了大量WordPress版本特定内容,属于重大版本更新)。