mcpField
W
wordpress-plugin-core

wordpress-plugin-core

github.com/jezweb开发1.1k560 次安装3 周前更新
代码仓库

关于 wordpress-plugin-core

面向 WordPress 插件开发者的开发技能,提供实现指导与安全要点。内容涵盖插件结构、WordPress 规范,以及输入处理、权限、数据库查询和输出的防护。

主要功能

  1. 01插件结构与架构模式
  2. 02唯一前缀和 ABSPATH 检查
  3. 03权限能力与 nonce 校验
  4. 04输入净化与输出转义
  5. 05预处理数据库查询及生命周期指导

适用场景

  1. 01搭建 WordPress 插件
  2. 02检查插件代码中的常见安全风险
  3. 03实现表单、AJAX 或 REST 端点

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill wordpress-plugin-core

安装后在 Claude Code 中输入/wordpress-plugin-core确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
common-hooks.md
example-reference.md
github-auto-updates.md
security-checklist.md
scripts/
example-script.sh
scaffold-plugin.sh
templates/plugin-oop/
README.md
templates/plugin-psr4/
README.md
composer.json
templates/plugin-simple/
README.md

详细介绍

WordPress 插件开发(核心)

最后更新:2026-01-21 最新版本:WordPress 6.9+(2025 年 12 月 2 日),建议使用 PHP 8.0+,兼容 PHP 8.5 依赖项:无(最低要求:WordPress 5.9+、PHP 7.4+)


快速入门

架构模式:简单模式(仅使用函数,<5 个函数)| OOP(中型插件)| PSR-4(现代/大型插件,2025 年及以后推荐)

插件头部(只需填写 Plugin Name):

<?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 项要点):

// 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 ) );

安全基础(详细说明)

唯一前缀(至少 4-5 个字符)

应用于:函数、类、常量、选项、瞬态、元键。避免使用:wp_、__、_。

function mypl_function() {}  // ✅
class MyPL_Class {}          // ✅
function init() {}           // ❌ Will conflict
能力检查(不要使用 is_admin())
// ❌ WRONG - Security hole
if ( is_admin() ) { /* delete data */ }

// ✅ CORRECT
if ( current_user_can( 'manage_options' ) ) { /* delete data */ }

常见能力:manage_options(管理员)、edit_posts(编辑者/作者)、read(订阅者)

安全三要素(输入 → 处理 → 输出)
// 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 ) . '">';
Nonce(CSRF 防护)
// 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' ),
) );
预处理语句
// ❌ 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 ) );

关键规则

务必做到

✅ 使用唯一前缀(4-5 个字符),用于所有全局代码(函数、类、选项、瞬态) ✅ 在每个 PHP 文件中添加 ABSPATH 检查:if ( ! defined( 'ABSPATH' ) ) exit; ✅ 检查能力(current_user_can()),而不只是检查 is_admin() ✅ 验证所有表单和 AJAX 请求的 nonce ✅ 对所有包含用户输入的数据库查询使用 $wpdb->prepare() ✅ 在保存输入前使用 sanitize_*() 函数清理 ✅ 在显示输出前使用 esc_*() 函数转义 ✅ 注册自定义文章类型时,在激活时刷新重写规则 ✅ 使用 uninstall.php 进行永久清理(不要使用停用钩子) ✅ 遵循 WordPress 编码标准(使用制表符缩进、Yoda 条件)

绝不做

❌ 绝不使用 extract()——它会造成安全漏洞 ❌ 未经清理,绝不信任 $_POST/$_GET ❌ 绝不将用户输入拼接到 SQL 中——务必使用 prepare() ❌ 绝不单独使用 is_admin() 进行权限检查 ❌ 绝不输出未经清理的数据——务必转义 ❌ 绝不使用通用函数/类名称——务必添加前缀 ❌ 绝不使用短 PHP 标签 <? 或 <?=——仅使用 <?php ❌ 停用时绝不删除用户数据——仅在卸载时删除 ❌ 绝不重复注册卸载钩子——仅在激活时注册一次 ❌ 绝不在主流程中使用 register_uninstall_hook()——改用 uninstall.php


已知问题预防

此技能可预防 29 个已记录的问题:

问题 #1:SQL 注入

错误:数据库因未转义的用户输入而遭到破坏 来源:https://patchstack.com/articles/sql-injection/(占所有漏洞的 15%) 发生原因:将用户输入直接拼接到 SQL 查询中 预防措施:务必使用带占位符的 $wpdb->prepare()

// 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'] ) );
问题 #2:XSS(跨站脚本攻击)

错误:恶意 JavaScript 在用户浏览器中执行 来源:https://patchstack.com(占所有漏洞的 35%) 发生原因:将未经清理的用户数据输出到 HTML 预防措施:务必使用适合上下文的函数转义输出

// VULNERABLE
echo $_POST['name'];
echo '<div class="' . $_POST['class'] . '">';

// SECURE
echo esc_html( $_POST['name'] );
echo '<div class="' . esc_attr( $_POST['class'] ) . '">';
问题 #3:CSRF(跨站请求伪造)

错误:未经授权,代替用户执行操作 来源:https://blog.nintechnet.com/25-wordpress-plugins-vulnerable-to-csrf-attacks/ 发生原因:未验证请求是否源自你的网站 预防措施:使用 wp_nonce_field() 和 wp_verify_nonce() 处理 nonce

// VULNERABLE
if ( $_POST['action'] == 'delete' ) {
    delete_user( $_POST['user_id'] );
}

// SECURE
if ( ! wp_verify_nonce( $_POST['nonce'], 'mypl_delete_user' ) ) {
    wp_die( 'Security check failed' );
}
delete_user( absint( $_POST['user_id'] ) );
问题 #4:缺少能力检查

错误:普通用户可以访问管理功能 来源:WordPress 安全审查指南 发生原因:使用 is_admin() 而非 current_user_can() 预防措施:务必检查用户能力,而不仅仅是管理界面上下文

// VULNERABLE
if ( is_admin() ) {
    // Any logged-in user can trigger this
}

// SECURE
if ( current_user_can( 'manage_options' ) ) {
    // Only administrators can trigger this
}
问题 #5:直接访问文件

错误:PHP 文件脱离 WordPress 上下文执行 来源:WordPress 插件手册 发生原因:文件顶部未检查 ABSPATH 预防措施:在每个 PHP 文件中添加 ABSPATH 检查

// Add to top of EVERY PHP file
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}
问题 #6:前缀冲突

错误:函数/类与其他插件冲突 来源:WordPress 编码规范 原因:使用了没有唯一前缀的通用名称 预防措施:为所有全局代码使用 4-5 个字符的前缀

// CAUSES CONFLICTS
function init() {}
class Settings {}
add_option( 'api_key', $value );

// SAFE
function mypl_init() {}
class MyPL_Settings {}
add_option( 'mypl_api_key', $value );
问题 #7:重写规则未刷新(以及性能问题)

错误:自定义文章类型返回 404 错误,或因反复刷新导致数据库过载 来源:WordPress 插件手册、Permalink Manager Pro 原因:注册 CPT 后忘记刷新重写规则,或在每次页面加载时都调用刷新 预防措施:仅在激活/停用时刷新,绝不要在每次页面加载时刷新

// ✅ 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
}

面向用户的修复方法:如果 CPT 显示 404,请前往“设置”→“固定链接”→“保存更改”手动刷新。

问题 #8:未清理 Transients

错误:数据库中累积了已过期的 Transients 来源:WordPress Transients API 文档 原因:卸载时没有清理 预防措施:在 uninstall.php 中删除 Transients

// 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_%'" );
问题 #9:脚本在所有页面加载

错误:不必要的资源加载降低了性能 来源:WordPress 性能最佳实践 原因:未进行条件检查就加入脚本/样式队列 预防措施:仅在需要的页面加载资源

// 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 );
    }
} );
问题 #10:保存时缺少净化

错误:恶意数据被存储在数据库中 来源:WordPress 数据验证 原因:未经净化就保存 $_POST 数据 预防措施:保存前始终进行净化

// VULNERABLE
update_option( 'mypl_setting', $_POST['value'] );

// SECURE
update_option( 'mypl_setting', sanitize_text_field( $_POST['value'] ) );
问题 #11:LIKE 查询不正确

错误:SQL 语法错误或注入漏洞 来源:WordPress $wpdb 文档 原因:未正确转义 LIKE 通配符 预防措施:使用 $wpdb->esc_like()

// WRONG
$search = '%' . $term . '%';

// CORRECT
$search = '%' . $wpdb->esc_like( $term ) . '%';
$results = $wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) );
问题 #12:使用 extract()

错误:变量冲突和安全漏洞 来源:WordPress 编码规范 原因:extract() 会根据数组键创建变量 预防措施:绝不要使用 extract(),直接访问数组元素

// DANGEROUS
extract( $_POST );
// Now $any_array_key becomes a variable

// SAFE
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';
问题 #13:REST API 缺少权限回调

错误:任何人都能访问端点,从而导致未授权访问或权限提升 来源:WordPress REST API 手册、Patchstack CVE 数据库 原因:未指定 permission_callback,或敏感端点缺少 show_in_index => false 预防措施:始终添加 permission_callback,并将敏感端点从 REST 索引中隐藏

2025-2026 年的真实漏洞:

  • All in One SEO(300 多万个站点):缺少权限检查,导致贡献者级别的用户可以查看全局 AI 访问令牌
  • AI Engine Plugin(CVE-2025-11749,CVSS 9.8 严重):未设置 show_in_index => false,导致承载令牌暴露在 /wp-json/ 索引中,未认证攻击者因此获得完整管理员权限
  • SureTriggers:披露后 4 小时内便因授权检查不足遭到利用
  • Worker for Elementor(CVE-2025-66144):订阅者级别的权限即可调用受限功能
// ❌ 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 年统计数据:共追踪到 64,782 个漏洞,一周内新增 333 个,236 个仍未修补。REST API 身份验证问题占据了相当大的比例。

问题 #14:重复注册卸载钩子

错误:每次页面加载都会写入选项 来源:WordPress 插件手册 原因:在主流程中调用 register_uninstall_hook() 预防措施:改用 uninstall.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
问题 #15:停用时删除数据

错误:用户暂时停用插件时丢失数据 来源:WordPress 插件开发最佳实践 原因:混淆了停用与卸载 预防措施:仅在 uninstall.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
问题 #16:使用已弃用的函数

错误:WordPress 更新后插件无法运行 来源:WordPress 已弃用函数列表 原因:使用了在较新 WordPress 版本中已移除的函数 预防措施:在开发期间启用 WP_DEBUG

// In wp-config.php (development only)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
问题 #17:文本域不匹配

错误:翻译无法加载 来源:WordPress 国际化 原因:文本域与插件 slug 不匹配 预防措施:在所有地方使用完全一致的插件 slug

// Plugin header
// Text Domain: my-plugin

// In code - MUST MATCH EXACTLY
__( 'Text', 'my-plugin' );
_e( 'Text', 'my-plugin' );
问题 #18:缺少插件依赖项

错误:所需插件未启用时发生致命错误 来源:WordPress 插件依赖项 原因:未检查所需插件 预防措施:在 plugins_loaded 上检查依赖项

add_action( 'plugins_loaded', function() {
    if ( ! class_exists( 'WooCommerce' ) ) {
        add_action( 'admin_notices', function() {
            echo '<div class="error"><p>My Plugin requires WooCommerce.</p></div>';
        } );
        return;
    }
    // Initialize plugin
} );
问题 #19:自动保存触发元数据保存

错误:元数据被多次保存,导致性能问题 来源:WordPress 文章元数据 原因:save_post 钩子中未检查自动保存 预防措施:检查 DOING_AUTOSAVE 常量

add_action( 'save_post', function( $post_id ) {
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // Safe to save meta
} );
问题 #20:admin-ajax.php 性能

错误:AJAX 响应缓慢 来源:https://deliciousbrains.com/comparing-wordpress-rest-api-performance-admin-ajax-php/ 原因:admin-ajax.php 会加载整个 WordPress 核心 预防措施:新项目使用 REST API(速度快 10 倍)

// 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' );
        },
    ) );
} );
问题 #21:区块编辑器缺少 show_in_rest

错误:自定义文章类型显示的是经典编辑器,而不是 Gutenberg 区块编辑器 来源:WordPress VIP 文档、GitHub 问题 #7595 原因:注册自定义文章类型时忘记设置 show_in_rest => true 预防措施:需要使用区块编辑器的 CPT 始终应包含 show_in_rest

// ❌ 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'),
) );

重要规则:只有注册时设置了 'show_in_rest' => true 的文章类型才兼容区块编辑器。区块编辑器依赖 WordPress REST API。对于与区块编辑器不兼容或设置了 show_in_rest => false 的文章类型,将改为加载经典编辑器。

问题 #22:wpdb::prepare() 表名转义

错误:表名被加引号导致 SQL 语法错误,或硬编码的前缀在不同安装环境中失效 来源:WordPress 编码标准问题 #2442 原因:将表名用作占位符会在表名两侧加上引号 预防措施:表名绝不能放入 prepare() 占位符中

// ❌ 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
) );
问题 #23:Nonce 验证边缘情况

错误:Nonce 验证失败导致用户体验困惑,或造成虚假的安全感 来源:MalCare:wp_verify_nonce()、Pressidium:理解 Nonce 发生原因:误解 Nonce 的行为和局限性 预防措施:了解 Nonce 的边缘情况,并始终将其与权限检查结合使用

边缘情况:

  1. 基于时间的返回值:
$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 可重复使用:WordPress 不会跟踪某个 Nonce 是否已被使用。在 12-24 小时的有效期内,同一个 Nonce 可以多次使用。

  2. 会话失效:Nonce 只有在绑定到有效会话时才有效。如果用户注销,所有 Nonce 都会失效;如果用户当时打开着表单,这会造成令人困惑的用户体验。

  3. 缓存问题:缓存插件提供较旧的 Nonce 时,缓存问题可能导致不匹配。

  4. 不能替代授权:

// ❌ 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'] ) );
}

关键原则(2025 年):绝不能依赖 Nonce 进行身份验证或授权。始终假设 Nonce 可能已泄露。使用 current_user_can() 保护函数。

问题 #24:钩子优先级和参数数量

错误:钩子回调未收到预期参数,或运行顺序不正确 来源:Kinsta:WordPress 钩子训练营 发生原因:默认只传递 1 个参数,默认优先级为 10 预防措施:需要时明确指定参数数量和优先级

// ❌ 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

最佳实践:

  • 始终为自定义钩子名称添加前缀,避免冲突:使用 do_action( 'mypl_data_processed' ),而非 do_action( 'data_processed' )
  • 过滤器必须 RETURN 修改后的数据,而不是将其输出
  • 钩子的位置会影响向后兼容性,因此要谨慎选择
问题 #25:自定义文章类型 URL 冲突

错误:即使刷新了固定链接,单篇 CPT 文章仍返回 404 错误 来源:Permalink Manager Pro:URL 冲突 发生原因:CPT slug 与页面 slug 相同,从而产生 URL 冲突 预防措施:为 CPT 使用不同的 slug,或重命名页面

// ❌ 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/
问题 #26:WordPress 6.8 bcrypt 密码哈希迁移

错误:升级 WordPress 6.8 后,自定义密码哈希处理失效 来源:WordPress Core Make、GitHub 问题 #21022 发生原因:WordPress 6.8 及更高版本将密码哈希算法从 phpass 切换为 bcrypt 预防措施:使用 WordPress 密码函数,不要直接处理哈希值

变更内容(WordPress 6.8,2025 年 4 月):

  • 默认密码哈希算法从 phpass 更改为 bcrypt
  • 新哈希前缀:$wp$2y$(经过 SHA-384 预哈希的 bcrypt)
  • 现有密码会在下次登录时自动重新哈希
  • 常用的 bcrypt 插件(roots/wp-password-bcrypt)现已多余
// ✅ 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)
}

需要采取的措施:

  • 检查直接处理密码哈希的插件
  • 升级到 6.8 及更高版本时,移除 bcrypt 插件
  • 如果标准使用 wp_hash_password/wp_check_password,则无需采取任何措施
问题 #27:WordPress 6.9 WP_Dependencies 弃用

错误:“弃用:调用 WP_Dependencies->add_data() 时使用了已弃用的参数” 来源:WordPress 6.9 文档,WordPress 支持论坛 发生原因:WordPress 6.9(2025 年 12 月 2 日)弃用了 WP_Dependencies 对象方法 预防措施:在启用 WP_DEBUG 的 WordPress 6.9 上测试插件,替换已弃用的方法

受影响的插件(已确认):

  • WooCommerce(已在 10.4.2 中修复)
  • Yoast SEO(已在 26.6 中修复)
  • Elementor(需要 3.24+)

破坏性变更:WordPress 6.9 移除或修改了若干旧主题和插件所依赖的已弃用函数,导致自定义菜单 walker、经典小工具、媒体模态框和自定义器功能出现问题。

需要采取的措施:

  • 在启用 WP_DEBUG 的 WordPress 6.9 上测试插件
  • 替换已弃用的 WP_Dependencies 方法
  • 检查 debug.log 中的弃用通知
  • 虽然排名前 1,000 的插件在数小时内就完成了修补,但无人维护的插件往往会滞后
问题 #28:WordPress 6.7 中的翻译加载变更

错误:翻译未加载或出现调试通知 来源:WooCommerce 开发者博客,WordPress 6.7 指南 发生原因:WordPress 6.7+ 更改了翻译的加载时间和方式 预防措施:在 'init' 优先级 10 之后加载翻译,确保文本域与插件 slug 相符

// ❌ 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

需要采取的措施:

  • 检查 load_plugin_textdomain() 的调用时机
  • 确保文本域与插件 slug 完全相符
  • 在启用 WP_DEBUG 的情况下进行测试
问题 #29:wpdb::prepare() 缺少占位符错误

错误:“wpdb::prepare() 的查询参数必须包含占位符” 来源:WordPress $wpdb 文档,SitePoint:使用数据库 发生原因:调用 prepare() 时未使用任何占位符 预防措施:如果没有动态数据,请勿使用 prepare()

// ❌ 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
) );

wpdb::prepare() 的其他常见错误:

  1. 百分号处理:
// ❌ 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. 混用参数格式:
// ❌ 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 ) );

插件架构模式

简单模式(仅使用函数)

小型插件(少于 5 个函数):

function mypl_init() { /* code */ }
add_action( 'init', 'mypl_init' );
OOP(单例)

中型插件:

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(现代模式,2025 年及以后推荐)

大型/团队插件:

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();

常见模式

自定义文章类型(重要:激活时刷新重写规则,在区块编辑器中启用 show_in_rest):

// 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
} );

自定义分类法:

register_taxonomy( 'genre', 'book', array( 'hierarchical' => true, 'show_in_rest' => true ) );

元数据框:

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'] ) );

设置 API:

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(比 admin-ajax.php 快 10 倍):

register_rest_route( 'myplugin/v1', '/data', array(
    'methods'             => 'POST',
    'callback'            => 'mypl_rest_callback',
    'permission_callback' => fn() => current_user_can( 'edit_posts' ),
) );

AJAX(旧版项目,新项目请使用 REST API):

add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' );
check_ajax_referer( 'mypl-ajax-nonce', 'nonce' );
wp_send_json_success( array( 'message' => 'Success' ) );

自定义表:

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

瞬态数据(缓存):

$data = get_transient( 'mypl_data' );
if ( false === $data ) {
    $data = expensive_operation();
    set_transient( 'mypl_data', $data, 12 * HOUR_IN_SECONDS );
}

附带资源

模板: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


高级主题

i18n(国际化):

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:

if ( defined( 'WP_CLI' ) && WP_CLI ) {
    WP_CLI::add_command( 'mypl', 'MyPL_CLI_Command' );
}

Cron 事件:

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' );

插件依赖项:

if ( ! class_exists( 'WooCommerce' ) ) {
    deactivate_plugins( plugin_basename( __FILE__ ) );
    add_action( 'admin_notices', fn() => echo '<div class="error"><p>Requires WooCommerce</p></div>' );
}

分发与自动更新

GitHub 自动更新(YahnisElsts 的 Plugin Update Checker):

// 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 );
}

部署:

git tag 1.0.1 && git push origin main && git push origin 1.0.1
# Create GitHub Release with ZIP (exclude .git, tests)

替代方案:Git Updater(无需编写代码)、自定义更新服务(完全掌控)、Freemius(商业方案)

安全性:使用 HTTPS,切勿将令牌硬编码,验证许可证,并限制更新检查频率

重要:ZIP 中必须包含插件文件夹:plugin.zip/my-plugin/my-plugin.php

资源:参见 references/github-auto-updates.md、examples/github-updater.php


依赖项

必需:

  • WordPress 5.9+(推荐 6.7+)
  • PHP 7.4+(推荐 8.0+)

可选:

  • Composer 2.0+ - 用于 PSR-4 自动加载
  • WP-CLI 2.0+ - 用于通过命令行管理插件
  • Query Monitor - 用于调试和性能分析

官方文档


故障排查

致命错误:启用 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,并确认语法正确


完整设置检查清单

使用此检查清单验证你的插件:

  • 插件头部信息完整,包含所有字段
  • 每个 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 弃用、翻译加载、REST API 2025-2026 年 CVE、wpdb::prepare() 边缘情况、nonce 限制、钩子注意事项、自定义文章类型 URL 冲突)。记录并预防的错误从 20 个增至 29 个。错误数量:20 → 29。版本:1.x → 2.0.0(因新增大量特定于 WordPress 版本的内容,故升级主版本号)。

评论

支持 Markdown