WordPress.org 插件提交技能
帮助你的插件顺利通过 wp.org 的 自动扫描 和 人工审核,无需反复上传五次。基于真实审核周期提炼而成(那种因为一个不起眼的 .gitkeep 就导致整个扫描失败的教训)。
维护者: moksa · moksaweb.com · 许可证:MIT
何时使用
当用户要求以下操作时触发此技能:
- 打包/构建一个干净的安装 zip 文件用于 wordpress.org
- 提交或重新提交插件到 wp.org 目录
- 修复 wp.org 审核团队的反馈(4 个经典类别)
- 诊断自动扫描拒绝原因(隐藏文件、前缀、数据库调用等)
- 一致地增加插件版本号(文件头 + readme + 常量)
- 上传前预检插件
不要用于:
- 非 WordPress 项目
- 不会提交到 wp.org 的私有/内部插件(审核规则不适用,但安全实践仍然有益)
两大关卡
- 自动扫描(上传时运行的 Plugin Check)—— 硬性阻止。必须通过才能接触人工审核。参见
references/automated-scan.md。 - 人工审核(自动扫描通过后由人类执行)—— 通过邮件发送问题,你修复后回复。几乎总是属于 4 个代码类别之一(
references/review-categories.md)以及 没有 linter 能捕捉的策略指南——许可、远程代码、跟踪、管理通知、商标、外部服务披露(references/guidelines.md)。
参考资料对照表
| 主题 | 文件 |
|---|---|
| Plugin Check (PCP) 操作手册——UI/CLI,全部 31 项检查 → 修复对照,错误与警告 | references/plugin-check.md |
| 自动扫描硬性阻止(隐藏文件、数据库、禁止调用) | references/automated-scan.md |
| 4 个人工审核类别 + 回复措辞 | references/review-categories.md |
| 真实审核教训(样例→全局搜索,3 种情况的 nonce,phpcs:ignore ≠ 转义,类型转换 ≠ 清理,路径,分支前缀) | references/real-review-lessons.md |
| 前缀化所有全局变量(函数/选项/钩子/句柄/自定义文章类型/元数据等) | references/prefixing.md |
清理/验证/转义 + $wpdb->prepare + Webhook 签名 | references/sanitization-escaping.md |
| 加载 JS/CSS(依赖/版本、内联、条件控制、块编辑器、无原始标签) | references/enqueue.md |
| 国际化(textdomain == 插件别名,文本参数为字面量,翻译器注释,JS) | references/i18n.md |
| 权限与权限回调(授权、REST、priv/nopriv) | references/capabilities.md |
| 策略指南(许可、远程代码、跟踪、管理通知、商标、披露) | references/guidelines.md |
| readme.txt 格式规范(文件头、标签≤5、简短描述≤150、截图、外部服务) | references/readme-txt.md |
| 能力 API/AI 客户端/MCP(每项能力的授权、数据边界、破坏性操作确认、提供商披露) | references/abilities-ai-mcp.md |
| 安全附加(文件/WP_Filesystem、上传、SSRF、禁止/混淆调用) | references/security-extras.md |
| 卸载与生命周期(数据清理、不删除用户内容、多站点) | references/uninstall.md |
| 打包 zip + 版本/文件头/readme 一致性 + .mo 文件 | references/packaging.md |
| 扫描/PHPCS 代码 → 修复查找 | data/plugin-check-errors.csv |
快速上传前核对清单(复制粘贴验证)
自动扫描(硬性阻止)
[ ] zip 中没有隐藏文件:`find . -name '.*' -not -name '.' -not -name '..'` 结果为空
(.gitkeep / .gitignore / .DS_Store / .editorconfig 都会导致 "hidden_files" 错误)
[ ] zip 中没有 node_modules/、.git/、tests/、scripts/、phpcs.xml、webpack.config.js
[ ] 所有插件拥有的全局变量使用唯一且 >=4 个字符的前缀(不能使用 wp_ / __ / 单个 _)
[ ] 没有未使用 prepare() 的直接 $wpdb 查询;没有远程代码执行;没有回传信息
人工审核(4 个类别)
[ ] 加载:没有原始的 echo '<script>'/'<style>';使用 wp_enqueue_* + wp_add_inline_*
[ ] 每个更改状态的 AJAX/admin_post 都使用 nonce + current_user_can;Webhook 在
使用前用 hash_equals 验证提供商签名(无法使用 WP nonce)
[ ] 每个 $_POST/$_GET 都经过清理(wp_unslash + sanitize_*);json_decode -> map_deep;
输出延迟转义(esc_html/esc_attr/esc_url/wp_kses)
[ ] textdomain == 插件别名;__() 参数为字面量字符串;%s 前有 /* translators: */ 注释
策略(人工审核,没有 linter 能捕捉这些——参见 references/guidelines.md)
[ ] 声明 GPL 兼容许可证 + 包含 LICENSE 文件;所有捆绑的库/资源均为 GPL 兼容
[ ] 无远程代码执行 / 不从 CDN 加载可执行 JS;资源本地打包
[ ] 任何第三方 API 调用在 readme "== External services ==" 中披露(什么数据、何时、URL)
[ ] 无未经明确选择加入的数据收集/跟踪(默认关闭);无前端信用
链接除非选择加入;管理通知可关闭 + 限定于特定页面(不滥用通知)
[ ] 插件别名/名称不以你未拥有商标的词开头(woocommerce-… / google-…)
README.TXT(参见 references/readme-txt.md——在 wp.org readme 验证器中验证)
[ ] 文件头解析:Contributors=wp.org 用户名,Tags≤5 个,Stable tag = 实际发布的版本
[ ] 简短描述 <=150 个字符,纯文本;变更日志有 Stable tag 对应的条目
[ ] 截图/横幅/图标放在 SVN /assets/ 目录中,不在 zip 内
AI / 能力 / MCP(如果插件注册了能力、调用了 LLM 或暴露了 MCP)
[ ] 每个 wp_register_ability 都有真实的 permission_callback(对象级权限),不是 __return_true
[ ] 数据边界:能力/MCP 工具只返回当前用户权限允许的数据
[ ] 模型/代理输入视为不可信;破坏性能力需确认后才执行
[ ] 捆绑了 LLM/AI SDK(GPL);在 == External services == 中披露提供商;密钥永不记录
[ ] MCP 端点需要认证(App Password/OAuth)并且执行每个能力的 permission_callback
额外安全与生命周期
[ ] 无 eval/shell/obfuscation/extract;无遗留的 var_dump/print_r/error_log
[ ] 仅通过 WP_Filesystem 将文件放到 uploads 目录;上传使用 wp_handle_upload + MIME 白名单
[ ] 通过 wp_safe_remote_* 进行远程调用(SSRF:阻止内部网络范围)+ 超时 + 响应检查
[ ] uninstall.php 受 WP_UNINSTALL_PLUGIN 保护;仅删除你前缀的数据;永不删除用户内容
打包与元数据
[ ] 版本同步:主文件头 Version、版本常量、readme Stable tag、变更日志
[ ] Requires 文件头在主文件 AND readme.txt 中保持一致(WP / PHP / WC)——以及任何
运行时在激活时检查的 MIN_* 常量
[ ] vendor/ 通过 `composer install --no-dev` 生成(不含 squizlabs/phpunit/phpcompatibility)
[ ] zip 根文件夹名称 == 插件别名
工作流程
- 阅读规则 了解任务(参见上方的参考资料对照表)——从
references/automated-scan.md+references/review-categories.md开始,然后深入与发现的问题对应的参考文件(prefixing.md/sanitization-escaping.md/enqueue.md/i18n.md),以及用于构建的references/packaging.md。 - 构建干净的 zip —— 运行
scripts/build-zip.sh(处理 git archive,移除开发文件 + 所有点文件,安装vendor/时使用--no-dev,以插件别名作为根目录压缩)。永远不要手动打包工作目录,否则开发垃圾和点文件会泄露进去。 - 预检 —— 运行
scripts/preflight.sh <path-to-zip-or-dir>:扫描隐藏文件,检查版本同步 + 文件头/readme 一致性,如果配置了 PHPCS 则运行。修复直到干净为止。 - 运行 Plugin Check (PCP) —— 这是官方工具,可以在本地运行目录的自动检查。
wp plugin check <slug> --require=./wp-content/plugins/plugin-check/cli.php(或工具 → Plugin Check)。修复每个 ERROR,检查每个 WARNING。参见references/plugin-check.md了解全部 31 项检查、标志以及每个技能章节对应修复什么问题。 - 上传。 如果自动扫描拒绝 → 读取准确的错误代码,通过
data/plugin-check-errors.csv映射,修复,重新构建,重新上传。 - 人工审核回复 —— 当审核者通过邮件发送问题时,在整个代码中逐一修复(示例只是样本——用 grep 搜索整个树,对整个插件运行 Plugin Check + PHPCS;相同类别重复出现 = 立即重新拒绝)。然后逐点回复,说明修改了什么。参见
references/review-categories.md了解标准修复 + 回复措辞,以及references/real-review-lessons.md了解每个类别的全局搜索命令和那些第一次修复时可能漏掉的陷阱。
来之不易的经验教训(代价高昂的那些)
.gitkeep会导致自动扫描失败。 它是隐藏文件。从包中去除所有点文件;从 git 中移除.gitkeep,这样git archive永远不会重新添加它。- 版本要求漂移。 wp.org 同时读取主文件文件头 AND readme.txt;运行时的
MIN_WC/MIN_PHP常量可以单独控制激活。三者必须一致,否则低版本用户安装成功后会遇到功能异常。 - Webhook 无法使用 WP nonce。 审核者会在 IPN 处理器上标记“无 nonce”——这是误报;正确的做法是通过
hash_equals()验证提供商签名。加上逐行的phpcs:ignore注释并说明原因。 json_decode不是清理操作。 始终对结果应用map_deep(sanitize_text_field)。composer install(不加--no-dev) 会将 phpcs/phpunit 泄露到 vendor/ 中 → 包体积增大,被标记。始终对发布包使用--no-dev。