wordpress-plugin-core

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WordPress 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:
manage_options
(Admin),
edit_posts
(Editor/Author),
read
(Subscriber)
php
// ❌ 错误 - 安全漏洞
if ( is_admin() ) { /* 删除数据 */ }

// ✅ 正确
if ( current_user_can( 'manage_options' ) ) { /* 删除数据 */ }
常用权限:
manage_options
(管理员)、
edit_posts
(编辑/作者)、
read
(订阅者)

Security 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:
if ( ! defined( 'ABSPATH' ) ) exit;
Check capabilities (
current_user_can()
) not just
is_admin()
Verify nonces for all forms and AJAX requests ✅ Use $wpdb->prepare() for all database queries with user input ✅ Sanitize input with
sanitize_*()
functions before saving ✅ Escape output with
esc_*()
functions before displaying ✅ Flush rewrite rules on activation when registering custom post types ✅ Use uninstall.php for permanent cleanup (not deactivation hook) ✅ Follow WordPress Coding Standards (tabs for indentation, Yoda conditions)
使用唯一前缀(4-5个字符)命名所有全局代码(函数、类、选项、临时数据) ✅ 为每个PHP文件添加ABSPATH检查
if ( ! defined( 'ABSPATH' ) ) exit;
检查权限
current_user_can()
),不要只依赖
is_admin()
验证所有表单和AJAX请求的Nonce所有包含用户输入的数据库查询都使用$wpdb->prepare()保存前使用
sanitize_*()
函数清理输入
显示前使用
esc_*()
函数转义输出
注册自定义文章类型时,在激活插件时刷新重写规则使用uninstall.php进行永久清理(不要用停用钩子) ✅ 遵循WordPress编码规范(用制表符缩进,Yoda条件判断)

Never Do

切勿执行

Never use extract() - Creates security vulnerabilities ❌ Never trust $_POST/$_GET without sanitization ❌ Never concatenate user input into SQL - Always use prepare() ❌ Never use
is_admin()
alone
for permission checks ❌ Never output unsanitized data - Always escape ❌ Never use generic function/class names - Always prefix ❌ Never use short PHP tags
<?
or
<?=
- Use
<?php
only ❌ Never delete user data on deactivation - Only on uninstall ❌ Never register uninstall hook repeatedly - Only once on activation ❌ Never use
register_uninstall_hook()
in main flow
- Use uninstall.php instead

永远不要使用extract() - 会产生安全漏洞 ❌ 未经过清理不要信任$_POST/$_GET数据永远不要将用户输入直接拼接进SQL语句 - 务必使用prepare() ❌ 永远不要仅用
is_admin()
做权限检查
永远不要输出未清理的数据 - 务必转义 ❌ 永远不要使用通用的函数/类名 - 务必添加前缀 ❌ 永远不要使用短PHP标签
<?
<?=
- 仅使用
<?php
永远不要在停用插件时删除用户数据 - 仅在卸载时删除 ❌ 永远不要重复注册卸载钩子 - 仅在激活时注册一次 ❌ 永远不要在主流程中使用
register_uninstall_hook()
- 改用uninstall.php

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
$wpdb->prepare()
with placeholders
php
// VULNERABLE
$wpdb->query( "DELETE FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" );

// SECURE
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) );
错误:通过未转义的用户输入导致数据库被攻陷 来源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
wp_nonce_field()
and
wp_verify_nonce()
php
// VULNERABLE
if ( $_POST['action'] == 'delete' ) {
    delete_user( $_POST['user_id'] );
}

// SECURE
if ( ! wp_verify_nonce( $_POST['nonce'], 'mypl_delete_user' ) ) {
    wp_die( 'Security check failed' );
}
delete_user( absint( $_POST['user_id'] ) );
错误:代表用户执行未授权操作 来源https://blog.nintechnet.com/25-wordpress-plugins-vulnerable-to-csrf-attacks/ 原因:未验证请求是否来自你的站点 预防:使用
wp_nonce_field()
wp_verify_nonce()
处理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
is_admin()
instead of
current_user_can()
Prevention: Always check capabilities, not just admin context
php
// VULNERABLE
if ( is_admin() ) {
    // Any logged-in user can trigger this
}

// SECURE
if ( current_user_can( 'manage_options' ) ) {
    // Only administrators can trigger this
}
错误:普通用户可访问管理员功能 来源: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
permission_callback
specified, or missing
show_in_index => false
for sensitive endpoints Prevention: Always add permission_callback AND hide sensitive endpoints from REST index
Real 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
    show_in_index => false
    , exposed bearer token in /wp-json/ index, full admin privileges granted to unauthenticated attackers
  • 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
,或敏感端点未设置
show_in_index => false
预防:始终添加permission_callback,并隐藏敏感端点的REST索引
2025-2026年真实漏洞:
  • All in One SEO(300万+站点):缺少权限检查,允许贡献者级别用户查看全局AI访问令牌
  • AI Engine插件(CVE-2025-11749,CVSS 9.8 严重):未设置
    show_in_index => false
    ,在/wp-json/索引中暴露Bearer令牌,未认证攻击者可获得完整管理员权限
  • 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.php

Issue #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
show_in_rest => true
when registering custom post type Prevention: Always include show_in_rest for CPTs that need block editor
php
// ❌ 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
'show_in_rest' => true
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
show_in_rest => false
—the classic editor will load instead.
错误:自定义文章类型显示经典编辑器而非Gutenberg块编辑器 来源WordPress VIP文档GitHub Issue #7595 原因:注册自定义文章类型时忘记设置
show_in_rest => true
预防:需要块编辑器的自定义文章类型务必包含show_in_rest
php
// ❌ 错误 - 块编辑器无法工作
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'),
) );
关键规则:只有注册时设置
'show_in_rest' => true
的文章类型才兼容块编辑器。块编辑器依赖WordPress REST API。对于不兼容块编辑器的文章类型——或设置
show_in_rest => false
的——会加载经典编辑器。

Issue #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:
  1. 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
  1. Nonce Reusability: WordPress doesn't track if a nonce has been used. They can be used multiple times within the 12-24 hour window.
  2. 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.
  3. Caching Problems: Cache issues can cause mismatches when caching plugins serve an older nonce.
  4. 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的边缘情况,并始终结合权限检查
边缘情况:
  1. 基于时间的返回值:
php
$result = wp_verify_nonce( $nonce, 'action' );
// 返回1: 有效,生成于0-12小时前
// 返回2: 有效,生成于12-24小时前
// 返回false: 无效或已过期
  1. Nonce可重用性: WordPress不会跟踪Nonce是否被使用过。在12-24小时的窗口期内,它们可以被多次使用。
  2. 会话失效: Nonce仅在绑定到有效会话时才有效。如果用户登出,他们的所有Nonce都会失效,如果用户之前打开了表单,会导致混乱的用户体验。
  3. 缓存问题: 缓存插件提供旧的Nonce时会导致不匹配。
  4. 不能替代授权:
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 last
Best Practices:
  • Always prefix custom hook names to avoid collisions:
    do_action( 'mypl_data_processed' )
    not
    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:
    $wp$2y$
    (SHA-384 pre-hashed bcrypt)
  • 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 MakeGitHub Issue #21022 原因:WordPress 6.8+ 从phpass切换为bcrypt密码哈希 预防:使用WordPress密码函数,不要直接处理哈希
变更内容(WordPress 6.8,2025年4月):
  • 默认密码哈希算法从phpass改为bcrypt
  • 新哈希前缀:
    $wp$2y$
    (SHA-384预哈希bcrypt)
  • 现有密码会在下次登录时自动重新哈希
  • 流行的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 exactly
Action 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:
  1. 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
) );
  1. 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()错误:
  1. 百分号处理:
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
) );
  1. 混合参数格式:
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.php
,
examples/settings-page.php
,
examples/custom-post-type.php
,
examples/rest-endpoint.php
,
examples/ajax-handler.php
Scripts:
scaffold-plugin.sh
,
check-security.sh
,
validate-headers.sh
References:
security-checklist.md
,
hooks-reference.md
,
sanitization-guide.md
,
wpdb-patterns.md
,
common-errors.md

模板:
plugin-simple/
,
plugin-oop/
,
plugin-psr4/
,
examples/meta-box.php
,
examples/settings-page.php
,
examples/custom-post-type.php
,
examples/rest-endpoint.php
,
examples/ajax-handler.php
脚本:
scaffold-plugin.sh
,
check-security.sh
,
validate-headers.sh
参考文档:
security-checklist.md
,
hooks-reference.md
,
sanitization-guide.md
,
wpdb-patterns.md
,
common-errors.md

Advanced 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 + escape
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' );
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.1
GitHub自动更新(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.1

Create 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

官方文档

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
wp_ajax_{action}
, check nonce sent/verified
HTML Stripped: Use
wp_kses_post()
not
sanitize_text_field()
for safe HTML
Query Fails: Use
$wpdb->prepare()
, check
$wpdb->prefix
, verify syntax

致命错误: 启用WP_DEBUG,检查wp-content/debug.log,验证带前缀的名称,检查依赖
自定义文章类型404: 通过设置 → 固定链接 → 保存来刷新重写规则
Nonce验证失败: 检查Nonce名称/动作是否匹配,验证是否过期(默认24小时)
AJAX返回0/-1: 验证动作名称与
wp_ajax_{action}
匹配,检查Nonce是否发送/验证
HTML被剥离: 对于安全HTML,使用
wp_kses_post()
而非
sanitize_text_field()
查询失败: 使用
$wpdb->prepare()
,检查
$wpdb->prefix
,验证语法

Complete Setup Checklist

完整设置检查清单

Use this checklist to verify your plugin:
  • Plugin header complete with all fields
  • ABSPATH check at top of every PHP file
  • All functions/classes use unique prefix
  • All forms have nonce verification
  • All user input is sanitized
  • All output is escaped
  • All database queries use $wpdb->prepare()
  • Capability checks (not just is_admin())
  • Custom post types flush rewrite rules on activation
  • Deactivation hook only clears temporary data
  • uninstall.php handles permanent cleanup
  • Text domain matches plugin slug
  • Scripts/styles only load where needed
  • WP_DEBUG enabled during development
  • Tested with Query Monitor for performance
  • No deprecated function warnings
  • Works with latest WordPress version

Questions? Issues?
  1. Check
    references/common-errors.md
    for extended troubleshooting
  2. Verify all steps in the security foundation
  3. Check official docs: https://developer.wordpress.org/plugins/
  4. Enable WP_DEBUG and check debug.log
  5. Use Query Monitor plugin to debug hooks and queries

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版本兼容

有疑问?有问题?
  1. 查看
    references/common-errors.md
    获取扩展故障排除指南
  2. 验证安全基础中的所有步骤
  3. 查看官方文档: https://developer.wordpress.org/plugins/
  4. 启用WP_DEBUG并检查debug.log
  5. 使用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版本特定内容,属于重大版本更新)。