# WordPress 插件开发手册

English original：WordPress Plugin Handbook (https://docs.cocobook.com/books/wp-plugin-handbook)  
WordPress 插件开发手册，覆盖钩子、设置、安全与组织提交流程。

# 插件手册

_欢迎来到 WordPress 插件开发者手册；您准备好进入 WordPress 插件的世界了吗？_

插件开发者手册是有关 WordPress 插件所有内容的资源。无论您是 WordPress 插件开发的新手，还是经验丰富的插件开发者，您都应该能在这里找到许多与插件相关问题的答案。

- 如果您是插件开发的新手，请从阅读[介绍](https://developer.wordpress.org/plugins/intro/)开始，然后继续学习[基础知识](https://developer.wordpress.org/plugins/plugin-basics/)。
- [插件安全](https://developer.wordpress.org/apis/security/)中的信息将介绍与安全相关的最佳实践。
- [钩子](https://developer.wordpress.org/plugins/hooks/)是让您的插件与 WordPress 交互的方式，也是您让其他开发者与您的插件交互的方式。
- [隐私](https://developer.wordpress.org/plugins/privacy/)将帮助您了解如何处理敏感数据。
- 要了解更多关于 WordPress 内置功能的信息，以便在您的插件中使用，请查看[管理菜单](https://developer.wordpress.org/plugins/administration-menus/)、[短代码](https://developer.wordpress.org/plugins/shortcodes/)、[设置](https://developer.wordpress.org/plugins/settings/)、[元数据](https://developer.wordpress.org/plugins/metadata/)、[自定义文章类型](https://developer.wordpress.org/plugins/post-types/)、[分类法](https://developer.wordpress.org/plugins/taxonomies/)和[用户](https://developer.wordpress.org/plugins/users/)。
- 了解如何使用 [HTTP API](https://developer.wordpress.org/plugins/http-api/) 获取数据。
- 如果您在插件中使用 [JavaScript、jQuery 或 AJAX](https://developer.wordpress.org/plugins/javascript/)，您将在该部分找到所需的信息。
- 要了解基于时间的 WordPress 任务，请查看 [Cron](https://developer.wordpress.org/plugins/cron/) 章节。
- [国际化](https://developer.wordpress.org/plugins/internationalization/)是让您的插件准备好用于您自己地区以外的其他地区的方式。
- 当所有这些都完成后，您可以为您的插件准备加入 [插件目录](https://developer.wordpress.org/plugins/wordpress-org/)。
- 最后：一些您可能会觉得有用的[开发者工具](https://developer.wordpress.org/plugins/developer-tools/)。

WordPress 插件开发者手册由 WordPress 社区创建，为 WordPress 社区服务。我们一直在寻找更多的贡献者；如果您有兴趣，请访问 [文档团队博客](https://make.wordpress.org/docs/) 了解更多关于参与的信息。

# 管理菜单

管理菜单是 WordPress 后台显示的界面。它们允许您为插件添加选项页面。

[info]关于管理导航菜单的信息，请参阅《主题开发者手册》中的导航菜单章节。[/info]

## 顶级菜单与子菜单

顶级菜单显示在 WordPress 后台的左侧。每个菜单可以包含一组子菜单。

在选择使用[顶级菜单](https://developer.wordpress.org/plugins/administration-menus/top-level-menus/)还是[子菜单](https://developer.wordpress.org/plugins/administration-menus/sub-menus/)时，请仔细考虑您的插件需求以及最终用户的需求。

[alert]我们建议只有一个选项页面的开发者将其作为子菜单添加到现有的顶级菜单中，例如设置或工具。[/alert]

# 子菜单

## 添加子菜单

要向 WordPress 管理后台添加新的子菜单，请使用 `add_submenu_page()` 函数。

```
add_submenu_page(
  string $parent_slug,
  string $page_title,
  string $menu_title,
  string $capability,
  string $menu_slug,
  callable $function = ''
);
```

### Example

Lets say we want to add a Sub-menu "WPOrg Options" to the "Tools" Top-level menu.

**The first step** will be creating a function which will output the HTML. In this function we will perform the necessary security checks and render the options we’ve registered using the [Settings API](https://developer.wordpress.org/plugins/settings/).

[info]We recommend wrapping your HTML using a `<div>` with a class of `wrap`.[/info]

```
function wporg_options_page_html() {
  // check user capabilities
  if ( ! current_user_can( 'manage_options' ) ) {
    return;
  }
  ?>
  <div class="wrap">
    <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
    <form action="options.php" method="post">
      <?php
      // output security fields for the registered setting "wporg_options"
      settings_fields( 'wporg_options' );
      // output setting sections and their fields
      // (sections are registered for "wporg", each field is registered to a specific section)
      do_settings_sections( 'wporg' );
      // output save settings button
      submit_button( __( 'Save Settings', 'textdomain' ) );
      ?>
    </form>
  </div>
  <?php
}
```

**The second step** will be registering our WPOrg Options Sub-menu. The registration needs to occur during the `admin_menu` action hook.

```
function wporg_options_page() {
  add_submenu_page(
    'tools.php',
    'WPOrg Options',
    'WPOrg Options',
    'manage_options',
    'wporg',
    'wporg_options_page_html'
  );
}
add_action( 'admin_menu', 'wporg_options_page' );
```

For a list of parameters and what each do please see the [`add_submenu_page()`](https://developer.wordpress.org/reference/functions/add_submenu_page/) in the reference.

## Predefined Sub-Menus

Wouldn’t it be nice if we had helper functions that define the `$parent_slug` for WordPress built-in Top-level menus and save us from manually searching it through the source code?

Below is a list of parent slugs and their helper functions:

- [`add_dashboard_page()`](https://developer.wordpress.org/reference/functions/add_dashboard_page/) – `index.php`
- [`add_posts_page()`](https://developer.wordpress.org/reference/functions/add_posts_page/) – `edit.php`
- [`add_media_page()`](https://developer.wordpress.org/reference/functions/add_media_page/) – `upload.php`
- [`add_pages_page()`](https://developer.wordpress.org/reference/functions/add_pages_page/) – `edit.php?post_type=page`
- [`add_comments_page()`](https://developer.wordpress.org/reference/functions/add_comments_page/) – `edit-comments.php`
- [`add_theme_page()`](https://developer.wordpress.org/reference/functions/add_theme_page/) – `themes.php`
- [`add_plugins_page()`](https://developer.wordpress.org/reference/functions/add_plugins_page/) – `plugins.php`
- [`add_users_page()`](administration-menus/sub-menus/hhttps:/developer.wordpress.org/reference/functions/add_users_page) – `users.php`
- [`add_management_page()`](https://developer.wordpress.org/reference/functions/add_management_page/) – `tools.php`
- [`add_options_page()`](https://developer.wordpress.org/reference/functions/add_options_page/) – `options-general.php`
- [`add_options_page()`](https://developer.wordpress.org/reference/functions/add_options_page/) – `settings.php`
- [`add_links_page()`](https://developer.wordpress.org/reference/functions/add_links_page/) – `link-manager.php` – requires a plugin since WP 3.5+
- Custom Post Type – `edit.php?post_type=wporg_post_type`
- Network Admin – `settings.php`

## 移除子菜单

移除子菜单的流程与[移除顶级菜单](https://developer.wordpress.org/plugins/administration-menus/top-level-menus/#remove-a-top-level-menu)完全相同。

## 提交表单

在子菜单中处理表单提交的过程与[顶级菜单中的表单提交](https://developer.wordpress.org/plugins/administration-menus/top-level-menus/#submitting-forms)完全相同。

`add_submenu_page()` 以及所有预定义子菜单的函数（`add_dashboard_page`、`add_posts_page` 等）都会返回一个 `$hookname`，你可以将其用作 `add_action` 的第一个参数，以便在自定义页面中处理表单提交：

```
function wporg_options_page() {
  $hookname = add_submenu_page(
    'tools.php',
    'WPOrg Options',
    'WPOrg Options',
    'manage_options',
    'wporg',
    'wporg_options_page_html'
    );

  add_action( 'load-' . $hookname, 'wporg_options_page_html_submit' );
}

add_action( 'admin_menu', 'wporg_options_page' );
```

一如既往，不要忘记检查表单是否正在提交，进行 CSRF 验证、[数据验证](https://developer.wordpress.org/apis/security/data-validation/)和[数据清理](https://developer.wordpress.org/apis/security/sanitizing/)。

# 顶级菜单

## 添加顶级菜单

要向 WordPress 管理后台添加新的顶级菜单，请使用 [`add_menu_page()`](https://developer.wordpress.org/reference/functions/add_menu_page/) 函数。

```
add_menu_page(
  string $page_title,
  string $menu_title,
  string $capability,
  string $menu_slug,
  callable $function = '',
  string $icon_url = '',
  int $position = null
);
```

### Example

Lets say we want to add a new Top-level menu called "WPOrg".

**The first step** will be creating a function which will output the HTML. In this function we will perform the necessary security checks and render the options we've registered using the [Settings API](https://developer.wordpress.org/plugins/settings/).

[info]We recommend wrapping your HTML using a `<div>` with a class of `wrap`.[/info]

```
function wporg_options_page_html() {
?>
  <div class="wrap">
    <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
    <form action="options.php" method="post">
      <?php
      // output security fields for the registered setting "wporg_options"
      settings_fields( 'wporg_options' );
      // output setting sections and their fields
      // (sections are registered for "wporg", each field is registered to a specific section)
      do_settings_sections( 'wporg' );
      // output save settings button
      submit_button( __( 'Save Settings', 'textdomain' ) );
      ?>
    </form>
  </div>
<?php
}
```

**The second step** will be registering our WPOrg menu. The registration needs to occur during the `admin_menu` action hook.

```
add_action( 'admin_menu', 'wporg_options_page' );
function wporg_options_page() {
  add_menu_page(
    'WPOrg',
    'WPOrg Options',
    'manage_options',
    'wporg',
    'wporg_options_page_html',
    plugin_dir_url(__FILE__) . 'images/icon_wporg.png',
    20
  );
}
```

For a list of parameters and what each do please see the [`add_menu_page()`](https://developer.wordpress.org/reference/functions/add_menu_page/) in the reference.

### 使用 PHP 文件生成 HTML

对于可移植代码的最佳实践是创建一个需要/包含你的 PHP 文件的回调函数。

为了完整性和帮助你理解遗留代码，我们将展示另一种方式：将 `PHP 文件路径` 作为 `$menu_slug` 参数传递，并将 `$function` 参数设为 `null`。

```
add_action( 'admin_menu', 'wporg_options_page' );
function wporg_options_page() {
  add_menu_page(
    'WPOrg',
    'WPOrg Options',
    'manage_options',
    plugin_dir_path(__FILE__) . 'admin/view.php',
    null,
    plugin_dir_url(__FILE__) . 'images/icon_wporg.png',
    20
  );
}
```

## 移除顶级菜单

要从 WordPress 管理界面移除已注册的菜单，请使用 [`remove_menu_page()`](https://developer.wordpress.org/reference/functions/remove_menu_page/) 函数。

```
remove_menu_page(
  string $menu_slug
);
```

[warning]移除菜单不会阻止用户直接访问它们。
这绝不应作为限制[用户能力](https://developer.wordpress.org/plugins/users/roles-and-capabilities/)的方式。[/warning]

### 示例

假设我们想要移除"工具"菜单。

```
add_action( 'admin_menu', 'wporg_remove_options_page', 99 );
function wporg_remove_options_page() {
  remove_menu_page( 'tools.php' );
}
```

在尝试移除菜单前，请确保该菜单已通过 `admin_menu` 钩子注册，并为 [`add_action()`](https://developer.wordpress.org/reference/functions/add_action/) 指定更高的优先级数值。

## 提交表单

要在选项页面处理表单提交，你需要完成以下两件事：

1. 使用页面 URL 作为表单的 `action` 属性
2. 添加一个钩子，使用 `add_menu_page` 返回的别名

[info]仅当你在后端手动创建表单时才需遵循这些步骤。推荐使用 [Settings API](https://developer.wordpress.org/plugins/settings/) 实现此功能。[/info]

### 表单操作属性

将选项页面的 `$menu_slug` 参数用作 [`menu_page_url()`](https://developer.wordpress.org/reference/functions/menu_page_url/) 函数的第一个参数。该函数默认会自动转义 URL 并输出，因此你可以直接在 `<form>` 标签内使用它：

```
<form action="<?php menu_page_url( 'wporg' ) ?>" method="post">
```

### 处理表单

在添加页面时指定的 `$function` 仅在显示页面时才会被调用，因此当需要向浏览器发送标头（例如重定向）时，此方法并不适用。

`add_menu_page` 会返回一个 `$hookname`，而 WordPress 会在任何 HTML 输出之前触发 `"load-$hookname"` 动作。你可以利用此机制来分配一个函数，该函数可用于处理表单。

[info]`"load-$hookname"` 将在每次显示选项页面之前执行，即使表单未被提交时也是如此。[/info]

考虑到返回参数和动作，上面的示例将如下所示：

```
add_action( 'admin_menu', 'wporg_options_page' );
function wporg_options_page() {
  $hookname = add_menu_page(
    'WPOrg',
    'WPOrg Options',
    'manage_options',
    'wporg',
    'wporg_options_page_html',
    plugin_dir_url(__FILE__) . 'images/icon_wporg.png',
    20
  );

  add_action( 'load-' . $hookname, 'wporg_options_page_submit' );
}
```

你可以根据需求编写 `wporg_options_page_submit` 函数，但请记住必须手动执行所有必要的检查，包括：

1. 表单是否正在提交（`'POST' === $_SERVER['REQUEST_METHOD']`）。
2. [CSRF 验证](https://developer.wordpress.org/apis/security/nonces/)
3. 验证
4. 数据清理

# 鸣谢

手动整理的鸣谢名单。

- @2Neil
- @aternus
- @awoods
- @blobaugh
- @code_poet
- @cyrijones
- @danielbachhuber
- @dawnmschaffer
- @drewapicture
- @endeavorm
- @grapplerulrich
- @hanni
- @hlashbrooke
- @iandunn
- @jasonm4563
- @jazzs3quence
- @joedolson
- @johnregan3
- @kimparsell
- @krogsgard
- @Marella
- @metripaldi9
- @mordauk
- @NemesisVex
- @netweb
- @NikV
- @nlarnold1
- @Otto42
- @pjackson1972
- @pollyplumber
- @ramiy
- @roccotripaldi
- @sewmyheadon
- @skippywp
- @theMikeD
- @tmoorewp
- @topher1kenobe

# Cron

## 什么是 WP-Cron

WP-Cron 是 WordPress 处理基于时间的任务调度的方式。WordPress 核心的多个功能，例如检查更新和发布定时文章，都使用了 WP-Cron。名称中的 "Cron" 部分来源于 UNIX 系统中可用的基于时间的任务调度系统 cron。

WP-Cron 的工作原理是：在每次页面加载时，检查预定的任务列表，以确定需要运行哪些任务。任何计划运行的任务都会在该页面加载期间被调用。

[info]WP-Cron 不像系统 cron 那样持续运行；它仅在页面加载时触发。[/info]

如果你计划在下午 2:00 运行一个任务，但直到下午 5:00 才有页面加载，则可能会出现调度错误。

## 为什么使用 WP-Cron

- WordPress 核心和许多插件需要一个调度系统来执行基于时间的任务。然而，许多托管服务是共享的，不提供对系统调度器的访问。
- 使用 WordPress API 来设置定时任务，比脱离 WordPress 去使用系统调度器更简单。
- 对于系统调度器，如果时间已过而任务未运行，它将不会重新尝试。而对于 WP-Cron，所有预定的任务都会被放入队列，并在下一次有机会时（即下一次页面加载时）运行。因此，虽然你无法 100% 确定任务**何时**运行，但你可以 100% 确定它**最终**会运行。

# 将 WP-Cron 接入系统任务调度器

如前所述，WP-Cron 不会持续运行，这对于需要准时执行的关键任务可能造成问题。对此有一个简单的解决方案：只需将系统任务调度器设置为按所需间隔（或在特定时间）运行。最简单的方案是使用工具向 `wp-cron.php` 文件发起网络请求。

在系统中设置好任务后，还需完成一个步骤。WordPress 仍会在每次页面加载时运行 WP-Cron，这已不再必要且会增加服务器资源消耗。可通过 `wp-config.php` 文件禁用 WP-Cron。打开 `wp-config.php` 文件进行编辑，添加以下行：

```
define( 'DISABLE_WP_CRON', true );
```

## Windows 系统

Windows 将其基于时间的调度系统称为"任务计划程序"，可通过控制面板的**管理工具**访问。

具体设置方法因服务器配置而异。一种方法是使用 PowerShell 和基本任务。创建基本任务后，可使用以下命令调用 WordPress Cron 脚本：

```
powershell "Invoke-WebRequest http://YOUR_SITE_URL/wp-cron.php"
```

## MacOS 与 Linux 系统

Mac OS X 和 Linux 均使用 cron 作为时间调度系统，通常通过终端执行 `crontab -e` 命令访问。需注意任务将根据执行命令的系统用户身份，以普通用户或 root 权限运行。

Cron 有特定的语法格式，包含以下部分：

- 分钟
- 小时
- 月份中的日期
- 月份
- 星期中的日期
- 要执行的命令

![Cron 调度示意图](https://i3.wp.com/developer.wordpress.org/files/2014/10/plugin-wp-cron-cron-scheduling.png)

若某个时间字段无需限制，应使用星号（`*`）。例如要每 15 分钟执行命令（忽略具体小时、日期或月份），可配置为：

多数服务器已安装 `wget` 工具，这是调用 WordPress Cron 脚本的便捷方式：

```
wget --delete-after https://example.com/wp-cron.php
```

[info]若未使用 –delete-after 参数，wget 将保存 HTTP GET 请求的输出结果。[/info]

每日午夜触发站点 WordPress Cron 的配置示例如下：

```
0 0 * * * wget --delete-after https://example.com/wp-cron.php
```

# 调度 WP Cron 事件

WP Cron 系统使用钩子来添加新的定时任务。

## 添加钩子

为了让你的任务能够运行，你必须创建自己的自定义钩子，并给这个钩子指定要执行的函数名。这是非常重要的一步。如果忘记，你的任务将永远不会运行。

以下示例将创建一个钩子。第一个参数是你正在创建的钩子名称，第二个是要调用的函数名称。

```
add_action( 'bl_cron_hook', 'bl_cron_exec' );
```

[info]请记住，函数名中的 `bl_` 部分是一个 _函数前缀_。你可以[在此](https://developer.wordpress.org/plugins/plugin-basics/best-practices/#prefix-everything)了解为什么前缀很重要。[/info]

[info]你可以[在此](https://developer.wordpress.org/plugins/hooks/actions/)阅读更多关于动作的信息。[/info]

## 任务调度

一个重要注意事项是 WP-Cron 是一个简单的任务调度器。我们知道，任务是通过创建的钩子来添加的，该钩子用于调用运行所需任务的函数。但是，如果您多次调用 `wp_schedule_event()`，即使使用相同的钩子名称，事件也会被多次调度。如果您的代码在每次页面加载时都添加任务，这可能导致任务被调度数千次。这并非您想要的结果。

WordPress 提供了一个便捷的函数 [`wp_next_scheduled()`](https://developer.wordpress.org/reference/functions/wp_next_scheduled/) 来检查特定钩子是否已被调度。`wp_next_scheduled()` 接受一个参数，即钩子名称。它将返回一个包含下次执行时间戳的字符串，或者返回 false，表示任务尚未调度。其用法如下：

```
wp_next_scheduled( 'bl_cron_hook' );
```

调度重复任务是通过 [`wp_schedule_event()`](https://developer.wordpress.org/reference/functions/wp_schedule_event/) 完成的。此函数接受三个必需参数，以及一个额外的参数，该参数是一个数组，可以传递给执行 wp-cron 任务的函数。我们将重点关注前三个参数。参数如下：

1. `$timestamp` – 此任务首次执行的 UNIX 时间戳。
2. `$recurrence` – 任务重复执行的时间间隔名称（以秒为单位）。
3. `$hook` – 要调用的自定义钩子名称。

我们将使用在[此处](https://developer.wordpress.org/plugins/cron/understanding-wp-cron-scheduling/)创建的 5 秒间隔以及上面创建的钩子，如下所示：

```
wp_schedule_event( time(), 'five_seconds', 'bl_cron_hook' );
```

请记住，我们首先需要确保任务尚未被调度。因此，我们将调度代码包装在如下检查中：

```
if ( ! wp_next_scheduled( 'bl_cron_hook' ) ) {
  wp_schedule_event( time(), 'five_seconds', 'bl_cron_hook' );
}
```

## 取消任务调度

当你不再需要某个已调度的任务时，可以使用 [`wp_unschedule_event()`](https://developer.wordpress.org/reference/functions/wp_unschedule_event/) 来取消任务调度。该函数接受以下两个参数：

1. `$timestamp` – 任务下一次执行的时间戳。
2. `$hook` – 要调用的自定义钩子名称。

此函数不仅会取消指定时间戳对应的任务，还会取消该任务所有未来的执行计划。由于你可能不知道下一个任务的时间戳，另一个实用函数 [`wp_next_scheduled()`](https://developer.wordpress.org/reference/functions/wp_next_scheduled/) 可以帮你找到它。`wp_next_scheduled()` 接受一个参数（我们关心的）：

1. `$hook` – 执行任务时调用的钩子名称。

整合起来的代码如下所示：

```
$timestamp = wp_next_scheduled( 'bl_cron_hook' );
wp_unschedule_event( $timestamp, 'bl_cron_hook' );
```

在不再需要任务时取消调度非常重要，因为 WordPress 会继续尝试执行这些任务，即使它们已不再使用（甚至在你的插件已停用或删除后）。一个需要记住取消任务调度的重要时机是在插件停用时。

遗憾的是，WordPress.org 插件目录中有许多插件没有自行清理。如果你发现这样的插件，请告知作者更新他们的代码。WordPress 提供了一个名为 [`register_deactivation_hook()`](https://developer.wordpress.org/reference/functions/register_deactivation_hook/) 的函数，允许开发者在插件停用时运行特定函数。设置非常简单，如下所示：

```
register_deactivation_hook( __FILE__, 'bl_deactivate' ); 

function bl_deactivate() {
  $timestamp = wp_next_scheduled( 'bl_cron_hook' );
  wp_unschedule_event( $timestamp, 'bl_cron_hook' );
}
```

[info]你可以在此处了解更多关于激活和停用钩子的信息[此处](https://developer.wordpress.org/plugins/plugin-basics/activation-deactivation-hooks/)。[/info]

# WP-Cron 测试

## WP-CLI

可以使用 [WP-CLI](https://wp-cli.org/) 测试定时任务。它提供了诸如 `wp cron event list` 和 `wp cron event run {job name}` 等命令。[查看文档](https://developer.wordpress.org/cli/commands/cron/) 获取更多详情。

## WP-Cron 管理插件

WordPress.org 插件目录中提供了多个[插件](https://wordpress.org/plugins/tags/cron/)，用于查看、编辑和控制您网站上的计划定时任务及可用计划。

## `_get_cron_array()`

[`_get_cron_array()`](https://developer.wordpress.org/reference/functions/_get_cron_array/) 函数返回所有当前计划定时任务的数组。如果您需要检查原始事件列表，请使用此函数。

## `wp_get_schedules()`

[`wp_get_schedules()`](https://developer.wordpress.org/reference/functions/wp_get_schedules/) 函数返回可用事件重复计划的数组。如果您需要检查原始可用计划列表，请使用此函数。

# Understanding WP-Cron Scheduling

Unlike a traditional system cron that schedules tasks for specific times (e.g. "every hour at 5 minutes past the hour"), WP-Cron uses intervals to simulate a system cron.

WP-Cron is given two arguments: the time for the first task, and an interval (in seconds) after which the task should be repeated. For example, if you schedule a task to begin at 2:00PM with an interval of 300 seconds (five minutes), the task would first run at 2:00PM and then again at 2:05PM, then again at 2:10PM, and so on, every five minutes.

To simplify scheduling tasks, WordPress provides some default intervals and an easy method for adding custom intervals.

The default intervals provided by WordPress are:

- hourly
- twicedaily
- daily
- weekly (since WP 5.4)

## Custom Intervals

To add a custom interval, you can create a filter, such as:

```
add_filter( 'cron_schedules', 'example_add_cron_interval' );
function example_add_cron_interval( $schedules ) { 
  $schedules['five_seconds'] = array(
    'interval' => 5,
    'display'  => esc_html__( 'Every Five Seconds' ),
  );
  return $schedules;
}
```

This filter function creates a new interval that will allow us to run a cron task every five seconds.

[info]All intervals are in seconds.[/info]

# 开发者工具

插件开发过程中可使用多种辅助工具。部分工具需在开发环境中运行（如 [xdebug](https://xdebug.org/)、[PHPCS](https://github.com/WordPress/WordPress-Coding-Standards) 等），同时也有优秀工具可直接在 WordPress 内部运行，帮助您规范构建项目并诊断问题。本章将重点介绍浏览器内置工具。

# Debug Bar 和插件

Debug Bar 是一个插件，它会在管理栏中添加一个调试菜单，显示查询、缓存和其他有用的调试信息。

## Debug Bar

这是主插件，提供了基础功能，本页列出的其他插件会在此基础上进行扩展。

当启用 `WP_DEBUG` 时，它还会跟踪 PHP 警告和通知，以便更容易找到它们。

当启用 `SAVEQUERIES` 时，会跟踪并显示 MySQL 查询。

[访问 Debug Bar](https://wordpress.org/plugins/debug-bar/)

## Debug Bar Console

此插件添加了一个控制台，您可以在其中运行任意 PHP 代码。这对于测试变量内容等众多用途来说非常出色。

[访问 Debug Bar Console](https://wordpress.org/plugins/debug-bar-console/)

## Debug Bar Shortcodes

此插件为 Debug Bar 添加一个新面板，显示当前请求中已注册的短代码。

此外，它还会显示：

- 短代码调用的函数/方法
- 短代码是否在当前文章/页面/文章类型中使用及使用方式（仅在单篇文章页面显示）
- 短代码的任何附加可用信息，例如描述、参数说明、是否为自闭合标签
- 查找使用特定短代码的所有页面/文章等内容

[访问 Debug Bar Shortcodes](https://wordpress.org/plugins/debug-bar-shortcodes/)

## 调试栏常量

此插件为调试栏添加三个新面板，显示当前请求中可供开发者使用的已定义常量：

- WP 常量
- WP 类常量
- PHP 常量

[访问调试栏常量](https://wordpress.org/plugins/debug-bar-constants/)

## Debug Bar 文章类型

此插件为 Debug Bar 添加一个新面板，显示您站点已注册文章类型的详细信息。

[访问 Debug Bar Post Types](https://wordpress.org/plugins/debug-bar-post-types/)

## Debug Bar Cron

此插件在 Debug Bar 中添加一个新面板，用于显示 WordPress 计划任务的相关信息。

安装后，您将可以访问以下信息：

- 计划任务的数量。
- 当前是否正在执行 cron。
- 下一个任务的时间。
- 当前时间。
- 自定义计划任务列表。
- 核心计划任务列表。
- 计划列表。

[访问 Debug Bar Cron](https://wordpress.org/plugins/debug-bar-cron/)

## Debug Bar 操作与过滤器插件

此插件在 Debug Bar 中新增两个标签页，用于显示当前请求所关联的钩子（操作与过滤器）。操作标签页展示当前请求所挂载的操作，过滤器标签页则显示过滤器标签及其关联函数与相应优先级。

[访问 Debug Bar 操作与过滤器插件](https://wordpress.org/plugins/debug-bar-actions-and-filters-addon/)

## 调试栏瞬态数据

此插件将 WordPress 瞬态数据信息添加至调试栏的新面板中。

安装后，您将可访问以下信息：

- 现有瞬态数据数量
- 自定义瞬态数据列表
- 核心瞬态数据列表
- 自定义站点瞬态数据列表
- 核心站点瞬态数据列表
- 删除瞬态数据的选项

[访问调试栏瞬态数据](https://wordpress.org/plugins/debug-bar-transients/)

## 调试栏脚本与样式依赖项列表

此插件可列出已加载的脚本和样式，显示其加载顺序及存在的依赖关系。

[访问调试栏脚本与样式依赖项列表](https://wordpress.org/plugins/debug-bar-list-dependencies/)

## Debug Bar 远程请求

此插件将为 Debug Bar 添加一个新面板，用于显示和分析通过 HTTP API 发起的远程请求。

安装后，您将可以访问以下信息：

- 请求方法（GET、POST 等）
- URL
- 每个请求的耗时
- 所有请求的总耗时
- 请求总数

您还可以在 URL 中添加 `?dbrr_full=1` 来获取额外信息，包括所有请求参数以及包含响应头的完整响应转储。

[访问 Debug Bar 远程请求](https://wordpress.org/plugins/debug-bar-remote-requests/)

# 辅助插件

## 插件检查工具

插件检查工具用于测试您的插件是否符合 WordPress.org 插件目录的必备标准。通过此插件，您可以运行新插件提交所需的大部分检查，并验证您的插件是否满足要求。

此外，该工具还会标记插件开发最佳实践中的违规行为或潜在问题，涵盖从国际化函数正确使用等基础要求，到可访问性、性能和安全最佳实践。

[访问插件检查工具](https://wordpress.org/plugins/plugin-check/)

## 查询监控器

查询监控器是面向 WordPress 开发者的调试插件。您可以查看数据库查询、钩子、条件判断、HTTP 请求、重定向等调试与性能信息。它具备其他调试插件所没有的高级功能，包括自动 AJAX 调试和按插件或主题筛选信息的能力。

[访问查询监控器](https://wordpress.org/plugins/query-monitor/)

# 钩子

钩子是一种让代码在特定预定义位置与其他代码交互/修改的方式。它们构成了插件和主题与 WordPress 核心交互的基础，同时核心自身也广泛使用钩子。

钩子有两种类型：[动作](https://developer.wordpress.org/plugins/hooks/actions/)和[过滤器](https://developer.wordpress.org/plugins/hooks/filters/)。要使用其中任何一种，您需要编写一个称为“回调函数”的自定义函数，然后将其注册到 WordPress 的特定动作或过滤器钩子。

[动作](https://developer.wordpress.org/plugins/hooks/actions/)允许您添加数据或改变 WordPress 的运行方式。动作会在 WordPress 核心、插件和主题执行的特定时刻运行。动作的回调函数可以执行某些任务，例如向用户输出内容或将数据插入数据库。动作的回调函数不会向调用它的动作钩子返回任何内容。

[过滤器](https://developer.wordpress.org/plugins/hooks/filters/)让您能够在 WordPress 核心、插件和主题执行期间修改数据。过滤器的回调函数会接收一个变量，修改它，然后将其返回。它们应以独立的方式工作，不应产生[副作用](https://en.wikipedia.org/wiki/Side_effect_(computer_science))，例如影响全局变量或输出。过滤器期望有内容返回给它们。

WordPress 提供了许多可用的钩子，但您也可以[创建自己的钩子](https://developer.wordpress.org/plugins/hooks/custom-hooks/)，以便其他开发者可以扩展和修改您的插件或主题。

## 动作与过滤器的区别

动作和过滤器的主要区别可以这样概括：

- 动作接收信息，对其进行某些操作，然后不返回任何内容。换句话说：它对某物执行操作，然后退出，不向调用钩子返回任何内容。
- 过滤器接收信息，以某种方式修改它，然后将其返回。换句话说：它过滤某物并将其传回钩子供后续使用。

另一种表述：

- 动作中断代码流程以执行某些操作，然后返回正常流程，不修改任何内容；
- 过滤器用于以特定方式修改某物，以便后续代码使用修改后的结果。

这里提到的“某物”是指通过钩子定义传递的参数列表。更多细节将在后续章节中介绍。

## 更多资源

- [过滤器参考](https://codex.wordpress.org/Plugin_API/Filter_Reference)
- [动作参考](https://codex.wordpress.org/Plugin_API/Action_Reference)

# 操作

**操作**是[钩子](https://developer.wordpress.org/plugins/hooks/)的两种类型之一。它们提供了一种在 WordPress 核心、插件和主题执行过程中的特定时刻运行函数的方式。操作的回调函数不会向调用它的操作钩子返回任何内容。它们是[过滤器](https://developer.wordpress.org/plugins/hooks/filters/)的对应物。这里回顾一下[操作与过滤器的区别](https://developer.wordpress.org/plugins/hooks/#actions-vs-filters)。

## 添加动作

添加动作的过程包含两个步骤：

### 创建 _回调函数_

首先，创建一个 _回调函数_。当它挂钩的动作运行时，此函数将被执行。

回调函数与普通函数类似：应添加前缀，并应位于 `functions.php` 或可调用的位置。它应接受的参数由您挂钩的动作定义；大多数钩子都有明确定义，因此请查阅钩子文档以了解所选动作将向您的函数传递哪些参数。

### Assign (_hook_) your callback function

Second, add your callback function to the action. This is called _hooking_ and tells the action to run your callback function when the action is run.

When your callback function is ready, use [add_action()](https://developer.wordpress.org/reference/functions/add_action/) to hook it to the action you have selected. At a minimum, `add_action()` requires two parameters:

1. `string $hook_name` which is the name of the action you're hooking to, and
2. `callable $callback` the name of your callback function.

The example below will run `wporg_callback()` when the `init` hook is executed:

```
function wporg_callback() {
  // do something
}
add_action( 'init', 'wporg_callback' );
```

You can refer to the [Hooks](https://developer.wordpress.org/plugins/hooks/) chapter for a list of available hooks.

As you gain more experience, looking through WordPress Core source code will allow you to find the most appropriate hook.

### 额外参数

`add_action()` 可接受两个额外参数：`int $priority` 用于指定回调函数的优先级，以及 `int $accepted_args` 用于设定传递给回调函数的参数数量。

#### 优先级

可以将多个回调函数挂载到同一个动作钩子上。例如 `init` 钩子就被广泛使用。在某些情况下，你可能需要确保你的回调函数在其他回调函数之前或之后运行，即使那些其他函数可能尚未被挂载。

WordPress 根据两个因素决定回调函数的运行顺序：第一种方式是通过手动设置**优先级**。这通过 `add_action()` 的第三个参数来实现。

以下是关于优先级的一些重要事实：

- 优先级是正整数，通常在 1 到 20 之间
- 默认优先级（即未手动提供 `priority` 值时分配的优先级）为 10
- 优先级值理论上没有上限，但实际使用中通常不超过 100

优先级为 11 的函数将在优先级为 10 的函数**之后**运行；优先级为 9 的函数将在优先级为 10 的函数**之前**运行。

决定回调函数顺序的第二种方式，是在**相同优先级值内**根据注册顺序确定。因此，如果两个回调函数以相同优先级注册到同一个钩子，它们将按照注册顺序依次运行。

例如，以下回调函数都注册到 `init` 钩子，但具有不同的优先级：

```
add_action( 'init', 'wporg_callback_run_me_late', 11 );
add_action( 'init', 'wporg_callback_run_me_normal' );
add_action( 'init', 'wporg_callback_run_me_early', 9 );
add_action( 'init', 'wporg_callback_run_me_later', 11 );
```

在上面的示例中：

- 首先运行的函数是 `wporg_callback_run_me_early()`，因为它的手动优先级为 9
- 接着是 `wporg_callback_run_me_normal()`，因为它未设置优先级，所以采用默认值 10
- 然后是 `wporg_callback_run_me_late()`，因为它的手动优先级为 11
- 最后是 `wporg_callback_run_me_later()`：它的优先级也是 11，但它在 `wporg_callback_run_me_late()` 之后被挂载

#### Number of Arguments

Sometimes it's desirable for a callback function to receive some extra data related to the action being hooked to.

For example, when WordPress saves a post and runs the [`save_post`](https://developer.wordpress.org/reference/hooks/save_post/) hook, it passes two parameters to the callback function: the ID of the post being saved, and the post object itself:

```
do_action( 'save_post', $post->ID, $post );
```

When a callback function is registered for the [`save_post`](https://developer.wordpress.org/reference/hooks/save_post/) hook, it can specify that it wants to receive those two parameters. It does so by telling `add_action` to expect them by (in this case) putting `2` as the fourth argument:

```
add_action( 'save_post', 'wporg_custom', 10, 2 );
```

In order to actually receive those parameters in your callback function, modify the parameters your callback function will accept, like this:

```
function wporg_custom( $post_id, $post ) {
  // do something
}
```

[tip]]It's good practice to give your callback function parameters the same name as the passed parameters, or as close as you can.[/tip]

# 高级主题

## 移除动作与过滤器

有时您需要从其他插件、主题甚至 WordPress 核心已注册的钩子中移除某个回调函数。

若要从钩子中移除回调函数，您需要调用 `remove_action()` 或 `remove_filter()`，具体取决于该回调函数是作为动作还是过滤器添加的。

传递给 `remove_action()` / `remove_filter()` 的参数必须与注册时传递给 `add_action()` / `add_filter()` 的参数完全一致，否则移除操作将无法生效。

[alert]要成功移除回调函数，必须在回调函数注册之后执行移除操作。执行顺序至关重要。[/alert]

## Example

Let's say we want to improve the performance of a large theme by removing unnecessary functionality.

Let's analyze the theme's code by looking into `functions.php`.

```
function wporg_setup_slider() {
  // ...
}
add_action( 'template_redirect', 'wporg_setup_slider', 9 );
```

The `wporg_setup_slider` function is adding a slider that we don't need, which probably loads a huge CSS file followed by a JavaScript initialization file which uses a custom written library the size of 1MB. We can can get rid of that.

Since we want to hook into WordPress after the `wporg_setup_slider` callback function was registered (`functions.php` executed) our best chance would be the [`after_setup_theme`](https://developer.wordpress.org/reference/hooks/after_setup_theme/) hook.

```
function wporg_disable_slider() {
  // Make sure all parameters match the add_action() call exactly.
  remove_action( 'template_redirect', 'wporg_setup_slider', 9 );
}
// Make sure we call remove_action() after add_action() has been called.
add_action( 'after_setup_theme', 'wporg_disable_slider' );
```

## Removing All Callbacks

You can also remove all of the callback functions associated with a hook by using `remove_all_actions()` / `remove_all_filters()`.

## Determining the Current Hook

Sometimes you want to run an Action or a Filter on multiple hooks, but behave differently based on which one is currently calling it.

You can use the `current_action()` / `current_filter()` to determine the current Action / Filter.

```
function wporg_modify_content( $content ) {
  switch ( current_filter() ) {
    case 'the_content':
      // Do something.
      break;
    case 'the_excerpt':
      // Do something.
      break;
  }
  return $content;
}

add_filter( 'the_content', 'wporg_modify_content' );
add_filter( 'the_excerpt', 'wporg_modify_content' );
```

## Checking How Many Times a Hook Has Run

Some hooks are called multiple times in the course of execution, but you may only want your callback function to run once.

In this situation, you can check how many times the hook has run with the [`did_action()`](https://developer.wordpress.org/reference/functions/did_action/).

```
function wporg_custom() {
  // If save_post has been run more than once, skip the rest of the code.
  if ( did_action( 'save_post' ) !== 1 ) {
    return;
  }
  // ...
}
add_action( 'save_post', 'wporg_custom' );
```

## 使用 "all" 钩子进行调试

如果你希望某个回调函数在每一个钩子上都触发，可以将其注册到 `all` 钩子。这在调试场景中有时很有用，可以帮助确定特定事件何时发生或页面何时崩溃。

```
function wporg_debug() {
  echo '<p>' . current_action() . '</p>';
}
add_action( 'all', 'wporg_debug' );
```

# 自定义钩子

一项重要但常被忽视的做法是在插件中使用自定义钩子，以便其他开发者能够扩展和修改它。

自定义钩子的创建和调用方式与 WordPress 核心钩子相同。

## 创建钩子

要创建自定义钩子，请对[操作](https://developer.wordpress.org/plugins/hooks/actions/)使用 [`do_action()`](https://developer.wordpress.org/reference/functions/do_action/)，对[过滤器](https://developer.wordpress.org/plugins/hooks/filters/)使用 [`apply_filters()`](https://developer.wordpress.org/reference/functions/apply_filters/)。

[info]我们建议对任何输出到浏览器的文本使用 [`apply_filters()`](https://developer.wordpress.org/reference/functions/apply_filters/)，尤其是在前端。

这使插件能更轻松地根据用户需求进行修改。[/info]

## 为钩子添加回调函数

要为自定义钩子添加回调函数，对[操作](https://developer.wordpress.org/plugins/hooks/actions/)使用[`add_action()`](https://developer.wordpress.org/reference/functions/add_action/)，对[过滤器](https://developer.wordpress.org/plugins/hooks/filters/)使用[`add_filter()`](https://developer.wordpress.org/reference/functions/add_filter/)。

## 命名冲突

当两位开发者将完全不同的功能使用相同的钩子名称时，就会发生命名冲突（"碰撞"）。这会导致难以排查的错误。因此，为你的钩子名称添加唯一的前缀非常重要，以避免与其他插件发生钩子名称冲突。

例如，名为 `email_body` 的过滤器过于通用，两位或更多开发者可能会在不同的插件中为不同目的使用此钩子。为避免这种情况，需要添加前缀。例如，本手册中的示例函数使用 `wporg_` 作为前缀。

选择前缀时，你可以使用公司名称、你的 WordPress 用户名、插件名称，或任何你喜欢的名称。目标是使其具有唯一性，因此请明智选择。

## 示例

### 可扩展操作：设置表单

如果您的插件向管理面板添加了设置表单，您可以使用操作（Actions）来允许其他插件向其中添加它们自己的设置。

```
do_action( 'wporg_after_settings_page_html' );
```

现在，另一个插件可以为 `wporg_after_settings_page_html` 钩子注册一个回调函数并注入新的设置：

```
add_action( 'wporg_after_settings_page_html', 'myprefix_add_settings' );
```

请注意，因为这是一个操作（action），所以不会返回值。同时请注意，由于没有指定优先级，它将以默认优先级 10 运行。

### 可扩展过滤器：自定义文章类型

在此示例中，当注册新的文章类型时，定义它的参数会通过过滤器传递，因此其他插件可以在文章类型创建之前修改这些参数。

```
function wporg_create_post_type() {
  $post_type_params = [/* ... */];

  register_post_type(
    'post_type_slug',
    apply_filters( 'wporg_post_type_params', $post_type_params )
  );
}
```

现在，另一个插件可以为 `wporg_post_type_params` 钩子注册一个回调函数并修改文章类型参数：

```
function myprefix_change_post_type_params( $post_type_params ) {
  $post_type_params['hierarchical'] = true;
  return $post_type_params;
}
add_filter( 'wporg_post_type_params', 'myprefix_change_post_type_params' );
```

请注意，过滤器接收数据、修改数据并返回数据。因此被调用的代码（`myprefix_change_post_type_params`）不会使用 echo 或 html 等方式直接向屏幕输出任何内容。同时注意，返回值直接被 `register_post_type` 使用，而无需先赋值给变量。这样可以简单地跳过那个额外（且不必要）的步骤。

还需注意，由于未指定优先级，它将以默认优先级 10 运行。并且由于未指定期望的参数数量，因此假定默认值为一个。

# 过滤器

**过滤器**是两种[钩子](https://developer.wordpress.org/plugins/hooks/)类型之一。

它们为函数提供了一种在 WordPress 核心、插件和主题执行期间修改数据的方式。它们是[动作](https://developer.wordpress.org/plugins/hooks/actions/)的对应部分。

与[动作](https://developer.wordpress.org/plugins/hooks/actions/)不同，过滤器应以独立方式工作，且不应产生[副作用](https://en.wikipedia.org/wiki/Side_effect_(computer_science))，例如影响全局变量和输出。过滤器期望有返回值。

## 添加过滤器

添加过滤器的过程包括两个步骤。

首先，您需要创建一个回调函数，该函数将在过滤器运行时被调用。其次，您需要将回调函数添加到一个将执行该函数调用的钩子中。

您将使用 [`add_filter()`](https://developer.wordpress.org/reference/functions/add_filter/) 函数，并至少传递两个参数：

1. `string $hook_name`：您要挂钩的过滤器名称，以及
2. `callable $callback`：您的回调函数名称。

以下示例将在 `the_title` 过滤器执行时运行。

```
function wporg_filter_title( $title ) {
  return 'The ' . $title . ' was filtered';
}
add_filter( 'the_title', 'wporg_filter_title' );
```

假设我们有一个文章标题 "Learning WordPress"，上述示例将把它修改为 "The Learning WordPress was filtered"。

您可以参考[钩子](https://developer.wordpress.org/plugins/hooks/)章节以获取可用钩子的列表。

随着经验的积累，查看 WordPress 核心源代码将帮助您找到最合适的钩子。

### 附加参数

[`add_filter()`](https://developer.wordpress.org/reference/functions/add_filter/) 可以接受两个附加参数：`int $priority` 用于指定回调函数的优先级，以及 `int $accepted_args` 用于指定将传递给回调函数的参数数量。

有关这些参数的详细说明，请阅读关于[动作](https://developer.wordpress.org/plugins/hooks/actions/)的文章。

### 示例

在满足特定条件时为 `<body>` 标签添加 CSS 类：

```
function wporg_css_body_class( $classes ) {
  if ( ! is_admin() ) {
    $classes[] = 'wporg-is-awesome';
  }
  return $classes;
}
add_filter( 'body_class', 'wporg_css_body_class' );
```

# HTTP API

## 简介

HTTP 代表超文本传输协议，是整个互联网的基础通信协议。即使这是您首次接触 HTTP，您可能已经理解得比您意识到的更多。在最基本的层面上，HTTP 的工作方式如下：

- “你好，服务器 XYZ，我可以获取文件 abc.html 吗？”
- “你好，小客户端，当然可以，给你。”

在 PHP 中有许多不同的方法来发送 HTTP 请求。WordPress HTTP API 的目的是支持尽可能多的这些方法，并为特定请求选择最合适的一种。

WordPress HTTP API 还可用于与其他 API（如 Twitter API 或 Google Maps API）进行通信和交互。

## HTTP 方法

HTTP 拥有多种方法（或称动词）来描述特定类型的操作。虽然存在更多方法，但 WordPress 已为三种最常用的方法预置了函数。每当发起 HTTP 请求时，都会附带一个方法，以帮助服务器判断客户端请求的是何种操作。

### GET

GET 用于获取数据。这是目前最常用的动词。每次你浏览网站或从 API 拉取数据时，你看到的都是 GET 请求的结果。实际上，你的浏览器向承载本文的服务器发送了一个 GET 请求，并请求了用于构建本文的数据。

### POST

POST 用于向服务器发送数据，以便服务器以某种方式处理。例如，联系表单。当您在表单字段中输入数据并点击提交按钮时，浏览器会获取数据，并将您输入表单的文本作为 POST 请求发送到服务器。随后，服务器将处理该联系请求。

### HEAD

HEAD 远不如另外两种方法知名。HEAD 本质上与 GET 请求相同，只是它不获取数据本身，仅获取数据的相关信息。这些信息描述了诸如数据最后更新时间、客户端是否应缓存数据、数据类型等。现代浏览器常向您先前访问过的页面发送 HEAD 请求，以判断是否有更新。如果没有，您实际看到的可能是先前下载的页面副本，从而避免不必要地消耗带宽重复获取相同内容。

所有优秀的 API 客户端都会在执行 GET 请求前先使用 HEAD，以节省带宽。虽然如果 HEAD 指示有新数据时需要进行两次独立的 HTTP 请求，但 GET 请求的数据量可能非常庞大。仅在 HEAD 指示数据是新的或不应缓存时才使用 GET，有助于节省宝贵的带宽并减少加载时间。

### 自定义方法

还存在其他 HTTP 方法，例如 PUT、DELETE、TRACE 和 CONNECT。本文不会涵盖这些方法，因为 WordPress 中没有预置的方法来使用它们，且 API 实现这些方法也尚不普遍。

根据服务器配置方式，您还可以实现自己的额外 HTTP 方法。使用非标准方法总是存在风险，并且会对其他开发者创建客户端来使用您的网站或 API 带来巨大限制，但在 WordPress 中确实可以使用任何您希望的方法。本文将简要介绍如何实现这一点。

## 响应码

HTTP 使用数字和字符串响应码。与其逐一详细解释，这里列出标准响应码。创建 API 时可以自定义响应码，但除非需要支持特定响应类型，否则最好遵循标准码。自定义码通常位于 1xx 范围。

### Code Classes

The type of response can quickly be seen by the leftmost digit of the three digit codes.

| Status Code | Description |
| --- | --- |
| 2xx | Request was successful |
| 3xx | Request was redirected to another URL |
| 4xx | Request failed due to client error. Usually invalid authentication or missing data |
| 5xx | Request failed due to a server error. Commonly missing or misconfigured configuration files |

**Common Codes**

These are the most common codes you will encounter.

| Status Code | Description |
| --- | --- |
| 200 | OK – Request was successful |
| 301 | Resource was moved permanently |
| 302 | Resource was moved temporarily |
| 403 | Forbidden – Usually due to an invalid authentication |
| 404 | Resource not found |
| 500 | Internal server error |
| 503 | Service unavailable |

## 从 API 获取数据

[GitHub](https://github.com/) 提供了一个出色的 API，其许多公开功能无需注册应用即可使用。因此，为了演示其中一些方法，示例将针对 GitHub API。

在 WordPress 中，通过 [`wp_remote_get()`](https://developer.wordpress.org/reference/functions/wp_remote_get/) 函数可以极其简单地获取数据。该函数接受以下两个参数：

1. `$url` – 要从中检索数据的资源。必须是标准的 HTTP 格式。
2. `$args` – 可选 – 您可以在此传递一个参数数组来更改行为和请求头，例如 cookies、跟随重定向等。

以下为默认设置，但可以通过 `$args` 参数进行更改：

- `method` – GET
- `timeout` – 5 – 放弃请求前的等待时间
- `redirection` – 5 – 跟随重定向的次数
- `httpversion` – 1.0
- `blocking` – true – 页面其余部分是否应等待此操作完成后再继续加载？
- `headers` – array()
- `body` – null
- `cookies` – array()

让我们使用一个 GitHub 用户账户的 URL，看看能获取到什么样的信息。

```
$response = wp_remote_get( 'https://api.github.com/users/blobaugh' );
```

`$response` 将包含我们请求的所有响应头、内容和其他元数据。

```
Array(
  [headers] => Array(
    [server] => nginx
    [date] => Fri, 05 Oct 2012 04:43:50 GMT
    [content-type] => application/json; charset=utf-8
    [connection] => close
    [status] => 200 OK
    [vary] => Accept
    [x-ratelimit-remaining] => 4988
    [content-length] => 594
    [last-modified] => Fri, 05 Oct 2012 04:39:58 GMT
    [etag] => "5d5e6f7a09462d6a2b473fb616a26d2a"
    [x-github-media-type] => github.beta
    [cache-control] => public, s-maxage=60, max-age=60
    [x-content-type-options] => nosniff
    [x-ratelimit-limit] => 5000
  )
  [body] => {"type":"User","login":"blobaugh","gravatar_id":"f25f324a47a1efdf7a745e0b2e3c878f","public_gists":1,"followers":22,"created_at":"2011-05-23T21:38:50Z","public_repos":31,"email":"ben@lobaugh.net","hireable":true,"blog":"http://ben.lobaugh.net","bio":null,"following":30,"name":"Ben Lobaugh","company":null,"avatar_url":"https://secure.gravatar.com/avatar/f25f324a47a1efdf7a745e0b2e3c878f?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png","id":806179,"html_url":"https://github.com/blobaugh","location":null,"url":"https://api.github.com/users/blobaugh"}
  [response] => Array(
    [preserved_text 5237511b45884ac6db1ff9d7e407f225 /] => 200
    [message] => OK
  )
  [cookies] => Array()
  [filename] =>
)
```

与此函数配合使用的所有辅助函数与前两个函数相同。唯一的例外是 HEAD 请求从不返回响应体，因此该元素将始终为空。

### 获取你梦寐以求的响应体

仅使用 [`wp_remote_retrieve_body()`](https://developer.wordpress.org/reference/functions/wp_remote_retrieve_body/) 即可获取响应体。此函数仅接受一个参数，即来自任何其他 [`wp_remote_X`](https://developer.wordpress.org/?s=wp_remote_&post_type%5B%5D=wp-parser-function) 函数的响应，其中 `retrieve` 不是紧随其后的值。

```
$response = wp_remote_get( 'https://api.github.com/users/blobaugh' );
$body     = wp_remote_retrieve_body( $response );
```

继续使用前面示例中的 GitHub 资源，`$body` 将是：

```
{"type":"User","login":"blobaugh","public_repos":31,"gravatar_id":"f25f324a47a1efdf7a745e0b2e3c878f","followers":22,"avatar_url":"https://secure.gravatar.com/avatar/f25f324a47a1efdf7a745e0b2e3c878f?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png","public_gists":1,"created_at":"2011-05-23T21:38:50Z","email":"ben@lobaugh.net","following":30,"name":"Ben Lobaugh","company":null,"hireable":true,"id":806179,"html_url":"https://github.com/blobaugh","blog":"http://ben.lobaugh.net","location":null,"bio":null,"url":"https://api.github.com/users/blobaugh"}
```

如果除了获取响应体之外，你不需要对响应执行任何其他操作，则可以将代码简化为一行：

```
$body = wp_remote_retrieve_body( wp_remote_get( 'https://api.github.com/users/blobaugh' ) );
```

许多类似的辅助函数都可以这样在一行中使用。

### 获取响应代码

您可能需要检查响应代码以确保检索成功。这可以通过 [`wp_remote_retrieve_response_code()`](https://developer.wordpress.org/reference/functions/wp_remote_retrieve_response_code/) 函数完成：

```
$response = wp_remote_get( 'https://api.github.com/users/blobaugh' );
$http_code = wp_remote_retrieve_response_code( $response );
```

如果成功，`$http_code` 将包含 `200`。

### 获取特定响应头

若您希望获取特定响应头，例如 last-modified，可以使用 [`wp_remote_retrieve_header()`](https://developer.wordpress.org/reference/functions/wp_remote_retrieve_header) 函数。此函数接受两个参数。

1. `$response` – 来自 GET 调用的响应。
2. `$header` – 要获取的响应头名称。

获取 last-modified 响应头的示例：

```
$response      = wp_remote_get( 'https://api.github.com/users/blobaugh' );
$last_modified = wp_remote_retrieve_header( $response, 'last-modified' );
```

`$last_modified` 将包含 `[last-modified] => Fri, 05 Oct 2012 04:39:58 GMT`

您也可以使用 `wp_remote_retrieve_headers( $response )` 以数组形式获取所有响应头。

### 使用基本认证的 GET 请求

受保护的 API 通常会提供多种不同类型的认证方式。HTTP 基本认证是一种常见但安全性不高的认证方法。在 WordPress 中，可以通过将 'Authorization' 传递给 [`wp_remote_get()`](https://developer.wordpress.org/reference/functions/wp_remote_get/) 函数的第二个参数（以及其他 HTTP 方法函数）来使用它。

```
$args = array(
  'headers' => array(
    'Authorization' => 'Basic ' . base64_encode( YOUR_USERNAME . ':' . YOUR_PASSWORD )
  )
);
wp_remote_get( $url, $args );
```

## 向 API 发送 POST 数据

所有 HTTP 方法调用都使用相同的辅助方法（[`wp_remote_retrieve_body()`](https://developer.wordpress.org/reference/functions/wp_remote_retrieve_body/) 等），并且使用方式也完全相同。

发送 POST 数据使用 [`wp_remote_post()`](https://developer.wordpress.org/reference/functions/wp_remote_post/) 函数，其参数与 [`wp_remote_get()`](https://developer.wordpress.org/reference/functions/wp_remote_get/) 完全一致。这里需要注意的是，您必须为第二个参数传入数组中的所有元素。Codex 提供了默认的可接受值。目前您只需关注要发送的数据，其他值将使用默认设置。

要向服务器发送数据，您需要构建一个关联数组。该数据将被赋值给 `'body'` 键。从服务器端来看，该值将如预期那样出现在 `$_POST` 变量中。例如，如果 `body => array( 'myvar' => 5 )`，那么在服务器端 `$_POST['myvar'] = 5`。

由于 GitHub 不允许向之前示例中使用的 API 发送 POST 请求，本示例将假设它可以。通常，如果您想向 API 发送 POST 数据，您需要联系 API 的维护者并获取 API 密钥或其他形式的身份验证令牌。这仅用于证明您的应用程序有权操作 API 上的数据，就像用户登录网站后对网站的操作一样。

假设我们正在提交一个包含以下字段的联系表单：姓名、电子邮件、主题、评论。要设置请求体，我们这样做：

```
$body = array(
  'name'    => 'Jane Smith',
  'email'   => 'some@email.com',
  'subject' => 'Checkout this API stuff',
  'comment' => 'I just read a great tutorial. You gotta check it out!',
);
```

现在我们需要设置将传递给 [`wp_remote_post()`](https://developer.wordpress.org/reference/functions/wp_remote_post/) 第二个参数的其他值。

```
$args = array(
  'body'        => $body,
  'timeout'     => '5',
  'redirection' => '5',
  'httpversion' => '1.0',
  'blocking'    => true,
  'headers'     => array(),
  'cookies'     => array(),
);
```

然后当然就是进行调用。

```
$response = wp_remote_post( 'https://example.com', $args );
```

## 避免带宽消耗

在获取资源前使用 HEAD 方法检查其状态非常重要，有时甚至是 API 的要求。对于高流量 API，GET 请求通常被限制为每分钟或每小时一定次数。除非 HEAD 请求显示 API 数据已更新，否则甚至无需尝试 GET 请求。

如前所述，HEAD 响应包含数据是否已更新、是否应缓存数据、缓存副本何时过期等信息，有时还会包含 API 请求的速率限制。

回到 GitHub 示例，以下是一些需要关注的响应头。大多数这些头部是标准化的，但您应始终查阅 API 文档以确保理解各头部的名称和用途。

- `x-ratelimit-limit` – 时间段内允许的请求数。
- `x-ratelimit-remaining` – 时间段内剩余的可用请求数。
- `content-length` – 内容大小（字节）。若内容较大，可用于提醒用户。
- `last-modified` – 资源最后修改时间。对缓存工具极为有用。
- `cache-control` – 客户端应如何处理缓存。

以下代码将检查我的 GitHub 用户账户的 HEAD 值：

```
$response = wp_remote_head( 'https://api.github.com/users/blobaugh' );
```

`$response` 应类似于：

```
Array(
  [headers] => Array(
    [server] => nginx
    [date] => Fri, 05 Oct 2012 05:21:26 GMT
    [content-type] => application/json; charset=utf-8
    [connection] => close
    [status] => 200 OK
    [vary] => Accept
    [x-ratelimit-remaining] => 4982
    [content-length] => 594
    [last-modified] => Fri, 05 Oct 2012 04:39:58 GMT
    [etag] => "5d5e6f7a09462d6a2b473fb616a26d2a"
    [x-github-media-type] => github.beta
    [cache-control] => public, s-maxage=60, max-age=60
    [x-content-type-options] => nosniff
    [x-ratelimit-limit] => 5000
  )
  [body] =>
  [response] => Array(
    [preserved_text 39a8515bd2dce2aa06ee8a2a6656b1de /] => 200
    [message] => OK
  )
  [cookies] => Array(
  )
  [filename] =>
)
```

与此前两种方法相同，所有辅助函数均可用于此函数。例外在于 HEAD 请求从不返回响应体，因此该元素将始终为空。

## 发起任意类型的请求

如果你需要使用上述函数不支持的 HTTP 方法发起请求，请不要慌张。优秀的 WordPress 开发者早已考虑到这一点，并贴心地提供了 [`wp_remote_request()`](https://developer.wordpress.org/reference/functions/wp_remote_request/)。此函数与 [`wp_remote_get()`](https://developer.wordpress.org/reference/functions/wp_remote_get/) 接收相同的两个参数，并允许你指定 HTTP 方法。需要传递哪些数据则取决于你的方法。

要发送 DELETE 方法请求，可以参考以下示例：

```
$args     = array(
  'method' => 'DELETE',
);
$response = wp_remote_request( 'http://some-api.com/object/to/delete', $args );
```

## 缓存简介

缓存是一种将常用对象或构建耗时较长的对象保存到快速对象存储中，以便后续请求快速检索的做法。这避免了再次花费时间获取和构建对象。缓存是一个广泛的主题，属于网站优化的一部分，本身就可以写成一个完整的系列文章。以下只是对缓存的介绍，以及一种简单而有效的方法来快速设置 API 响应缓存。

为什么要缓存 API 响应？嗯，房间里的大象是因为外部 API 会拖慢你的网站。许多顾问会告诉你，接入外部 API 可以通过减少连接和处理量以及昂贵的带宽来提高网站性能，但有时这并不正确。

这需要在服务器发送数据的速度与远程服务器处理请求、构建数据并返回所需时间之间进行微妙的平衡。第二个明显的问题是，许多 API 在特定时间段内的请求数量有限，并且可能限制应用程序同时的连接数。缓存通过将数据副本存储在服务器上直到需要刷新，有助于解决这些难题。

## 何时应该缓存？

直觉的回答是**总是缓存**，但同样存在不应缓存的情况。如果您处理的是实时数据，或者 API 在响应头中明确声明不应缓存，那么您可能不希望缓存；但在所有其他情况下，缓存从 API 获取的资源通常是个好主意。

## WordPress 瞬态数据

WordPress 瞬态数据提供了一种便捷的方式来存储和使用缓存对象。瞬态数据会存活指定的时间，或在 API 资源更新后按需过期。使用 WordPress 的瞬态功能可能是您遇到过的最简单的缓存系统，仅需三个函数即可完成所有繁重工作。

### 缓存对象（设置瞬态数据）

使用 [`set_transient()`](https://developer.wordpress.org/reference/functions/set_transient/) 函数可以缓存对象。该函数接受以下三个参数：

1. `$transient` – 用于后续引用的瞬态数据名称。
2. `$value` – 瞬态数据的值。
3. `$expiration` – 从保存瞬态数据到其过期的时间（以秒为单位）。

例如，将上述 GitHub 用户信息响应缓存一小时：

```
$response = wp_remote_get( 'https://api.github.com/users/blobaugh' );
set_transient( 'prefix_github_userinfo', $response, 60 * 60 );
```

### Get a cached object ( Get a transient )

Getting a cached object is quite a bit more complex than setting a transient. You need to request the transient, but then you also need to check to see if that transient has expired and if so fetch updated data. Usually the `set_transient()` call is made inside of the `get_transient()` call. Here is an example of getting the transient data for the GitHub user profile:

```
$github_userinfo = get_transient( 'prefix_github_userinfo' );
if ( false === $github_userinfo ) {
  // Transient expired, refresh the data
  $response = wp_remote_get( 'https://api.github.com/users/blobaugh' );
  set_transient( 'prefix_github_userinfo', $response, HOUR_IN_SECONDS );
}
// Use $github_userinfo as you will
```

## 删除缓存对象（删除瞬态数据）

删除缓存对象是所有瞬态函数中最简单的操作，只需传入瞬态名称参数即可完成。

要删除 Github 用户信息：

```
delete_transient( 'blobaugh_github_userinfo' );
```

更多关于瞬态数据的信息可查阅[此文档](https://developer.wordpress.org/apis/transients/)。

# 国际化

## 什么是国际化？

国际化是指开发插件时使其能够轻松翻译成其他语言的过程。国际化通常缩写为 `i18n`（因为在字母 i 和 n 之间有 18 个字母）。

## 为什么国际化很重要？

WordPress 在全球各地被使用，许多国家的主要语言并非英语。WordPress 插件中的字符串需要以特殊方式编码，以便能够轻松翻译成其他语言。作为开发者，您可能无法为所有用户提供*本地化*（即针对特定*区域*（地点）所需的文本更改和其他事项，如数字格式）；然而，翻译人员可以成功本地化主题，而无需修改源代码本身。

进一步阅读[如何国际化您的插件](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/)。

## 资源

- [视频：i18n：让您的 WordPress 主题面向世界](https://www.youtube.com/watch?v=fJfqgrzjEis)
- [视频：关于国际化：面向全世界的插件和主题](https://wordpress.tv/2014/02/26/samuel-otto-wood-on-internationalization-plugins-and-themes-for-the-whole-world/)
- [视频：在日本大受欢迎：主题和国际化的指南](https://wordpress.tv/2013/08/03/shannon-smith-big-in-japan-a-guide-for-themes-and-internationalization/)
- [视频：翻译迷失——i18n 和 WordPress](https://wordpress.tv/2009/11/14/ze-fontainhas-i18n-nyc09/)
- [国际化和本地化您的 WordPress 主题](https://www.smashingmagazine.com/2011/12/internationalizing-localizing-wordpress-theme/)
- [国际化：您可能做错了](http://ottopress.com/2012/internationalization-youre-probably-doing-it-wrong/)
- [更多国际化趣事](http://ottopress.com/2012/more-internationalization-fun/)
- [使用 `wp_localize_script`，它很棒](https://pippinsplugins.com/use-wp_localize_script-it-is-awesome/)
- [理解](https://konstantin.blog/2013/_n_noop/) [`_n_noop()`](https://developer.wordpress.org/reference/functions/_n_noop/)
- [语言包 101 – 准备工作](http://ottopress.com/2013/language-packs-101-prepwork/)
- [翻译 WordPress 插件和主题：不要耍小聪明](https://markjaquith.wordpress.com/2011/10/06/translating-wordpress-plugins-and-themes-dont-get-clever/)
- [如何加载主题和插件翻译](https://ulrich.pogson.ch/load-theme-plugin-translations)
- [多语言者手册：插件/主题作者指南](https://make.wordpress.org/polyglots/handbook/plugin-theme-authors-guide/)

# 如何实现插件国际化

要使应用程序中的字符串可翻译，您需要将原始字符串包裹在一组特殊函数调用中。这些函数统称为"gettext"。

## Gettext 简介

WordPress 使用 [gettext](https://www.gnu.org/software/gettext/) 库和工具进行国际化，但并非直接使用：为此专门创建了一组特殊函数来实现字符串翻译。这些函数如下所列。您应在插件中使用这些函数。

如需深入了解 gettext，请阅读 [gettext 在线手册](https://www.gnu.org/software/gettext/manual/html_node/)。

## 文本域

使用 _文本域_ 来标识属于您插件的所有文本。文本域是一个唯一标识符，确保 WordPress 能够区分所有已加载的翻译。这提高了可移植性，并能更好地与现有的 WordPress 工具配合使用。

文本域必须与插件的 `slug` 匹配。如果您的插件是名为 `my-plugin.php` 的单个文件，或者包含在名为 `my-plugin` 的文件夹中，则域名必须是 `my-plugin`。如果您的插件托管在 wordpress.org 上，则必须是插件 URL 的 slug 部分（`wordpress.org/plugins/my-plugin`）。

文本域名必须使用连字符而非下划线，全部小写，且不能包含空格。

文本域还需要添加到插件头部。WordPress 使用它来国际化您的插件元数据，即使插件处于禁用状态。文本域应与[加载文本域](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#loading-text-domain)时使用的文本域相同。

### 标题示例

```
/* 
 * Plugin Name: My Plugin
 * Author: Plugin Author
 * Text Domain: my-plugin
 */
```

[info]再次提醒，将 "my-plugin" 更改为你的插件别名。[/info]

[info]自 WordPress 4.6 起，`Text Domain` 标题是可选的，因为它必须与插件别名相同。包含它没有坏处，但不是必需的。[/info]

## 域路径

域路径定义了插件翻译文件的位置。这有几个用途，特别是让 WordPress 在插件被禁用时仍能找到翻译文件。默认情况下，它指向插件所在的文件夹。

例如，如果翻译文件位于插件内名为 `languages` 的文件夹中，那么域路径就是 `/languages`，并且必须以斜杠开头：

### 标题示例

```
/*
 * Plugin Name: My Plugin
 * Author: Plugin Author
 * Text Domain: my-plugin
 * Domain Path: /languages
 */
```

[info]如果插件位于官方的 WordPress 插件目录中，可以省略 `Domain Path` 标题。[/info]

## 基础字符串

对于基础字符串（指不包含占位符或复数形式的字符串），请使用 [`__()`](https://developer.wordpress.org/reference/functions/__/)。该函数返回其参数的翻译：

```
__( 'Blog Options', 'my-plugin' );
```

[warning]请勿在 gettext 函数的文本域部分使用变量名或常量。例如：不要使用以下快捷方式：

`__( 'Translate me.' , $text_domain );`[/warning]

若要直接输出已检索的翻译，请使用 [`_e()`](https://developer.wordpress.org/reference/functions/_e/)。因此，无需编写：

```
echo __( 'WordPress is the best!', 'my-plugin' );
```

您可以使用：

```
_e( 'WordPress is the best!', 'my-plugin' );
```

## Variables

What if you have a string like the following:

```
echo 'Your city is $city.'
```

In this case, the `$city` is a variable and should not be part of the translation. The solution is to use placeholders for the variable, along with the `printf` family of functions. Especially helpful are [`printf`](https://www.php.net/manual/en/function.printf.php) and [`sprintf`](https://www.php.net/manual/en/function.sprintf). Here is what the right solution looks like:

```
printf(
  /* translators: %s: Name of a city */
  __( 'Your city is %s.', 'my-plugin' ),
  $city
);
```

Notice that here the string for translation is just the template `"Your city is %s."`, which is the same both in the source and at run-time.

Also note that there is a hint for translators so that they know the context of the placeholder.

If you have more than one placeholder in a string, it is recommended that you use [argument swapping](http://www.php.net/manual/en/function.sprintf.php). In this case, single quotes (`'`) around the string are mandatory because double quotes (`"`) will tell php to interpret the `$s` as the `s` variable, which is not what we want.

```
printf(
  /* translators: 1: Name of a city 2: ZIP code */
  __( 'Your city is %1$s, and your zip code is %2$s.', 'my-plugin' ),
  $city,
  $zipcode
);
```

Here the zip code is being displayed after the city name. In some languages displaying the zip code and city in opposite order would be more appropriate. Using %s prefix in the above example, allows for such a case. A translation can thereby be written:

```
printf(
  /* translators: 1: Name of a city 2: ZIP code */
  __( 'Your zip code is %2$s, and your city is %1$s.', 'my-plugin' ),
  $city,
  $zipcode
);
```

**Important!** The following code is incorrect:

```
// This is incorrect do not use.
_e( "Your city is $city.", 'my-plugin' );
```

The strings for translation are extracted from the sources, so the translators will get this phrase to translate: `"Your city is $city."`.

However in the application `_e` will be called with an argument like `"Your city is London."` and `gettext` won't find a suitable translation of this one and will return its argument: `"Your city is London."`. Unfortunately, it isn't translated correctly.

## 复数形式

### 基本复数化处理

如果字符串会随项目数量变化而改变，您需要在翻译中体现这一点。例如，英语中有 `"One comment"` 和 `"Two comments"`。在其他语言中可能存在多种复数形式。在 WordPress 中处理这种情况，请使用 [`_n()`](https://developer.wordpress.org/reference/functions/_n/) 函数。

```
printf(
  _n(
    '%s comment',
    '%s comments',
    get_comments_number(),
    'my-plugin'
  ),
  number_format_i18n( get_comments_number() )
);
```

`_n()` 接受 4 个参数：

- singular – 字符串的单数形式（请注意在某些语言中，单数形式可能用于非 1 的数字，因此应使用 `'%s item'` 而非 `'One item'`）。
- plural – 字符串的复数形式。
- count – 对象数量，用于确定应返回单数还是复数形式（有些语言的复数形式远超过 2 种）。
- text domain – 插件的文本域。

函数的返回值是根据给定数量对应的正确翻译形式。

请注意，有些语言会将单数形式用于其他数字（例如 21、31 等，类似于英语中的 "21st"、"31st"）。如果您想特别处理单数情况，请单独检查：

```
if ( 1 === $count ) {
  printf( esc_html__( 'Last thing!', 'my-text-domain' ), $count );
} else {
  printf( esc_html( _n( '%d thing.', '%d things.', $count, 'my-text-domain' ) ), $count );
}
```

另请注意，`$count` 参数通常被使用两次。首先将 `$count` 传递给 `_n()` 以确定使用哪个翻译字符串，然后将 `$count` 传递给 `printf()` 以将数字代入翻译后的字符串中。

### 稍后处理复数形式

首先使用 [`_n_noop()`](https://developer.wordpress.org/reference/functions/_n_noop/) 或 [`_nx_noop()`](https://developer.wordpress.org/reference/functions/_nx_noop/) 设置复数字符串。

```
$comments_plural = _n_noop(
  '%s 条评论。',
  '%s 条评论。'
);
```

然后在代码的后续位置，可以使用 [`translate_nooped_plural()`](https://developer.wordpress.org/reference/functions/translate_nooped_plural/) 来加载这些字符串。

```
printf(
  translate_nooped_plural(
    $comments_plural,
    get_comments_number(),
    'my-plugin'
  ),
  number_format_i18n( get_comments_number() )
);
```

## 通过上下文消除歧义

有时一个术语会在多个上下文中使用，尽管在英语中是同一个单词，但在其他语言中需要不同的翻译。例如，单词 `Post` 既可以用作动词 `"点击此处发布您的评论"`，也可以用作名词 `"编辑此文章"`。在这种情况下，应使用 `_x()` 或 `_ex()` 函数。它类似于 `__()` 和 `_e()`，但有一个额外的参数——上下文：

```
_x( 'Post', 'noun', 'my-plugin' );
_x( 'Post', 'verb', 'my-plugin' );
```

在这两种情况下使用此方法，原始版本将得到字符串 Comment，但翻译人员将看到两个用于翻译的 Comment 字符串，每个字符串处于不同的上下文中。

请注意，与 `__()` 类似，`_x()` 也有一个 `echo` 版本：`_ex()`。前面的示例可以写成：

```
_ex( 'Post', 'noun', 'my-plugin' );
_ex( 'Post', 'verb', 'my-plugin' );
```

使用您认为能增强可读性和编码便利性的任何一种。

## 说明

为了让翻译者知道如何翻译像 `__( 'g:i:s a' )` 这样的字符串，你可以在源代码中添加一个说明性注释。它必须以 `translators:` 开头，并且必须是 gettext 调用前的最后一个 PHP 注释。以下是一个示例：

```
/* translators: 草稿保存日期格式，参见 http://php.net/date */
$saved_date_format = __( 'g:i:s a' );
```

它也用于解释字符串中的占位符，例如 `_n_noop( '<strong>版本 %1$s</strong> 修复了 %2$s 个错误。','<strong>版本 %1$s</strong> 修复了 %2$s 个错误。' )`。

```
/* translators: 1: WordPress 版本号, 2: 错误数量的复数形式。 */
_n_noop( '<strong>版本 %1$s</strong> 修复了 %2$s 个错误。','<strong>版本 %1$s</strong> 修复了 %2$s 个错误。' );
```

## 换行符

Gettext 不欢迎可翻译字符串中出现 `r`（ASCII 码：13），请避免使用它，改用 `n`。

## 空字符串

空字符串被 Gettext 内部使用所保留，请勿尝试国际化空字符串。这也没有任何意义，因为翻译者将看不到任何上下文。

如果您有国际化空字符串的合理用例，请添加上下文，既帮助翻译者，又与 Gettext 系统和谐共处。

## 字符串转义

建议对所有字符串进行转义处理，这样翻译人员就无法运行恶意代码。国际化的函数中内置了几种转义函数。

## Localization functions

### Basic functions

- [`__()`](https://developer.wordpress.org/reference/functions/__/)
- [`_e()`](https://developer.wordpress.org/reference/functions/_e/)
- [`_x()`](https://developer.wordpress.org/reference/functions/_x/)
- [`_ex()`](https://developer.wordpress.org/reference/functions/_ex/)
- [`_n()`](https://developer.wordpress.org/reference/functions/_n/)
- [`_nx()`](https://developer.wordpress.org/reference/functions/_nx/)
- [`_n_noop()`](https://developer.wordpress.org/reference/functions/_n_noop/)
- [`_nx_noop()`](https://developer.wordpress.org/reference/functions/_nx_noop/)
- [`translate_nooped_plural()`](https://developer.wordpress.org/reference/functions/translate_nooped_plural/)

### Translate & Escape functions 

Strings that require translation and is used in attributes of html tags must be escaped.

- [`esc_html__()`](https://developer.wordpress.org/reference/functions/esc_html__/)
- [`esc_html_e()`](https://developer.wordpress.org/reference/functions/esc_html_e/)
- [`esc_html_x()`](https://developer.wordpress.org/reference/functions/esc_html_x/)
- [`esc_attr__()`](https://developer.wordpress.org/reference/functions/esc_attr__/)
- [`esc_attr_e()`](https://developer.wordpress.org/reference/functions/esc_attr_e/)
- [`esc_attr_x()`](https://developer.wordpress.org/reference/functions/esc_attr_x/)

### 日期与数字函数

- [`number_format_i18n()`](https://developer.wordpress.org/reference/functions/number_format_i18n/)
- [`date_i18n()`](https://developer.wordpress.org/reference/functions/date_i18n/)

## 字符串编写最佳实践

以下是编写字符串的最佳实践

- 使用得体的英语风格——尽量减少俚语和缩写
- 使用完整句子——大多数语言的语序与英语不同
- 按段落拆分——合并相关句子，但不要将整页文本放在一个字符串中
- 不要在可翻译短语中保留首尾空格
- 假设翻译后字符串长度可能翻倍
- 避免使用特殊标记和非常规控制字符——不要包含包围文本的标签
- 不要在翻译字符串中添加不必要的 HTML 标记
- 除非可能有其他语言版本，否则不要将 URL 留给翻译
- 在字符串中添加变量占位符，因为某些语言中占位符位置会变化

```
printf(
  __( '搜索结果：%s', 'my-plugin' ),
  get_search_query()
);
```

- 使用格式化字符串而非字符串连接——翻译短语而非单词——`printf( __( '您所在城市是 %1$s，邮政编码是 %2$s。', 'my-plugin' ), $city, $zipcode );` 总是优于：`__( '您所在城市是 ', 'my-plugin' ) . $city . __( '，邮政编码是 ', 'my-plugin' ) . $zipcode;`
- 尽量使用相同的词语和符号，避免多个字符串需要翻译，例如 `__( '文章：', 'my-plugin' );` 和 `__( '文章', 'my-plugin' );`

### 为字符串添加文本域

您必须将文本域作为参数添加到每个 `__()`、`_e()` 和 `__n()` 的 gettext 调用中，否则您的翻译将无法工作。

示例：

- `__( 'Post' )` 应改为 `__( 'Post', 'my-theme' )`。
- `_e( 'Post' )` 应改为 `_e( 'Post', 'my-theme' )`。
- `_n( '%s post', '%s posts', $count )` 应改为 `_n( '%s post', '%s posts', $count, 'my-theme' )`。

如果您的插件中有与 WordPress 核心相同的字符串（例如“Settings”），您仍然需要为其添加自己的文本域，否则当核心字符串更改时（这种情况会发生），这些字符串将无法翻译。

如果在编写代码时没有持续添加文本域，手动添加可能会很繁琐，这就是为什么您可以自动完成此操作：

- 将 [`add-textdomain.php`](https://develop.svn.wordpress.org/branches/5.2/tools/i18n/add-textdomain.php) 脚本下载到您要添加文本域的文件所在的文件夹中。
- 在命令行中，移动到该文件所在的目录。
- 运行以下命令以创建一个添加了文本域的新文件：

```
php add-textdomain.php my-plugin my-plugin.php > new-my-plugin.php
```

如果您希望将 `add-textdomain.php` 放在不同的文件夹中，只需在命令中定义其位置即可。

```
php /path/to/add-textdomain.php my-plugin my-plugin.php > new-my-plugin.php
```

如果您不想输出新文件，请使用此命令：

```
php add-textdomain.php -i my-plugin my-plugin.php
```

如果您想更改目录中的多个文件，也可以将目录传递给脚本：

```
php add-textdomain.php -i my-plugin my-plugin-directory
```

完成后，文本域将被添加到文件中所有 gettext 调用的末尾。如果已存在文本域，则不会被替换。

## 加载文本域

可以使用 `load_plugin_textdomain` 加载翻译，例如：

```
add_action( 'init', 'wpdocs_load_textdomain' );

function wpdocs_load_textdomain() {
  load_plugin_textdomain( 'wpdocs_textdomain', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
```

## WordPress.org 上的插件

[info]自 WordPress 4.6 起，翻译现在优先使用 [translate.wordpress.org](https://translate.wordpress.org/)，因此通过 translate.wordpress.org 翻译的插件不再必须使用 `load_plugin_textdomain()`。如果您不想在插件中添加 `load_plugin_textdomain()` 调用，则必须在 readme.txt 中将 `Requires at least:` 字段设置为 4.6 或更高版本。[/info]

如果您仍想加载自己的翻译而非来自 translate 的翻译，则必须使用名为 `load_textdomain_mofile` 的钩子过滤器。  
**示例**：在插件的 `/languages/` 目录中放置 .mo 文件，并将以下代码插入主插件文件：

```
function my_plugin_load_my_own_textdomain( $mofile, $domain ) {
  if ( 'my-domain' === $domain && false !== strpos( $mofile, WP_LANG_DIR . '/plugins/' ) ) {
    $locale = apply_filters( 'plugin_locale', determine_locale(), $domain );
    $mofile = WP_PLUGIN_DIR . '/' . dirname( plugin_basename( __FILE__ ) ) . '/languages/' . $domain . '-' . $locale . '.mo';
  }
  return $mofile;
}
add_filter( 'load_textdomain_mofile', 'my_plugin_load_my_own_textdomain', 10, 2 );
```

## 处理 JavaScript 文件

请查阅 [通用 API 手册](https://developer.wordpress.org/apis/) 中的 [JavaScript 国际化](https://developer.wordpress.org/apis/internationalization/#internationalizing-javascript) 部分，了解如何正确加载翻译文件。另请参阅 [Gutenberg 插件文档页面](https://github.com/WordPress/gutenberg/blob/trunk/docs/how-to-guides/internationalization.md)。

## 语言包

如果您对语言包以及如何导入到 [translate.wordpress.org](https://translate.wordpress.org/) 感兴趣，请阅读 [Meta 手册中关于翻译的页面](https://make.wordpress.org/meta/handbook/tutorials-guides/translations/)。

同时，请参考 [Polyglots 手册中的插件/主题作者指南](https://make.wordpress.org/polyglots/handbook/plugin-theme-authors-guide/) 来获取项目的翻译支持。

# 本地化

## 什么是本地化？

本地化描述的是国际化插件后续的翻译过程。本地化常被缩写为 `l10n`（因为 l 和 n 之间有 10 个字母）。

## 本地化文件

### POT（可移植对象模板）文件

此文件包含您插件中的原始字符串（英文）。

### PO（可移植对象）文件

每位译者将获取 `POT` 文件，并将 `msgstr` 部分翻译为自己的语言。最终生成与 `POT` 格式相同但包含译文及特定标头的 `PO` 文件。每种语言对应一个 `PO` 文件。

### MO（机器对象）文件

每个翻译后的 `PO` 文件都会生成一个 `MO` 文件。这些是机器可读的二进制文件，gettext 函数实际使用它们（它们不关心 `.POT` 或 `.PO` 文件），是 `PO` 文件的“编译”版本。转换通过 `msgfmt` 命令行工具完成。通常，一个应用程序可能使用多个大型逻辑可翻译模块，并相应地使用不同的 `MO` 文件。文本域是每个模块的句柄，对应不同的 `MO` 文件。

## 生成 POT 文件

`POT` 文件是您需要交给翻译人员以便他们开展工作的文件。`POT` 和 `PO` 文件可以轻松地通过重命名互换，从而更改文件类型，不会出现问题。

[info]建议将 POT 文件与您的插件一同提供，这样翻译人员就不必专门向您索要。[/info]

为您的插件生成 `POT` 文件有几种方法：

### WP-CLI

安装 [WP-CLI](https://make.wordpress.org/cli/handbook/guides/installing/)，并根据[文档](https://developer.wordpress.org/cli/commands/i18n/make-pot/)使用 `wp i18n make-pot` 命令。

### Poedit

你也可以在本地翻译时使用 [Poedit](https://poedit.net/)。这是一款适用于所有主流操作系统的开源工具。免费的 Poedit 默认版本支持手动扫描所有包含 Gettext 函数的源代码。其专业版还提供了一键扫描 WordPress 插件的功能。生成 `PO` 文件后，你可以将其重命名为 `POT`。如果生成了 `MO` 文件，可以将其删除，因为它不是必需的。如果你没有专业版，可以轻松获取 [Blank POT](https://github.com/fxbenard/Blank-WordPress-Pot) 并将其用作 `POT 文件` 的基础。将空白 `POT` 文件放入语言文件夹后，你可以在 Poedit 中点击“更新”来用你的字符串更新 `POT` 文件。

### Grunt 任务

甚至还有一些可用于创建 POT 文件的 Grunt 任务：[grunt-wp-i18n](https://github.com/cedaro/grunt-wp-i18n) 和 [grunt-pot](https://www.npmjs.com/package/grunt-pot)。设置 Grunt 的步骤超出了本文档的范围，但请注意这是可行的。这里有一个[示例 Grunt.js 和 package.json](https://gist.github.com/grappler/10187003)，你可以将其放在插件的根目录中。

## 翻译 PO 文件

将翻译后的文件保存为 `my-plugin-{locale}.mo`。locale 是语言代码和/或国家代码。例如，德语的 locale 是 `de_DE`。根据上面的代码示例，文本域是 'my-plugin'，因此德语的 MO 和 PO 文件应命名为 `my-plugin-de_DE.mo` 和 `my-plugin-de_DE.po`。有关语言和国家代码的更多信息，请参阅[以您的语言安装 WordPress](https://developer.wordpress.org/advanced-administration/before-install/in-your-language/)。

翻译 `PO` 文件有多种方法。

### 手动翻译

您可以使用文本编辑器输入翻译内容。在文本编辑器中，其格式如下所示。

```
#: plugin-name.php:123
msgid "Page Title"
msgstr ""
```

您需要在引号之间输入翻译内容。例如，德语翻译将如下所示。

```
#: plugin-name.php:123
msgid "Page Title"
msgstr "Seitentitel"
```

### Poedit

翻译时也可以使用 [Poedit](https://poedit.net/)。免费的 Poedit 默认版本支持手动扫描所有包含 Gettext 函数的源代码。其专业版还提供一键扫描 WordPress 插件和主题的功能。

### 在线服务

第三种选择是使用在线翻译服务。基本思路是上传 `POT` 文件，然后授权用户或翻译人员翻译你的插件。这能让你追踪变更、始终获取最新翻译，并避免重复翻译工作。

以下是一些可用于在线翻译 PO 文件的工具：

- [Transifex](https://www.transifex.com/)
- [WebTranslateIt](https://webtranslateit.com/)
- [Poeditor](https://poeditor.com/)
- [GlotPress](https://wordpress.org/plugins/glotpress/)

## 生成 MO 文件

### 命令行

`msgfmt` 用于创建 MO 文件。`msgfmt` 是 Gettext 软件包的一部分。也可以使用命令行。典型的 `msgfmt` 命令如下所示：

#### Unix 操作系统

```
msgfmt -o filename.mo filename.po
```

#### Windows 操作系统

```
msgfmt -o filename.mo filename.po
```

如果你需要批量转换多个 `PO` 文件，可以将其作为批处理任务运行。例如，使用 `bash` 命令：

#### Unix 操作系统（多文件）

```
# 查找 PO 文件，使用 msgfmt 处理每个文件，并将结果重命名为 MO
for file in `find . -name "*.po"` ; do msgfmt -o ${file/.po/.mo} $file ; done
```

#### Windows 操作系统（多个文件）

对于 Windows 系统，你需要先安装 [Cygwin](https://www.cygwin.com/)。

创建一个名为 `potomo.sh` 的文件，并将以下内容放入其中：

```
#! /bin/sh
# 查找 PO 文件，用 msgfmt 处理每个文件，并将结果重命名为 MO
for file in `/usr/bin/find . -name '*.po'` ; do /usr/bin/msgfmt -o ${file/.po/.mo} $file ; done
```

你可以在命令行中运行此命令。

```
cd C:/path/to/language/folder/my-plugin/languages & C:/cygwin/bin/bash -c /cygdrive/c/path/to/script/directory/potomo.sh
```

### Poedit

`msgfmt` 也集成在 [Poedit](https://poedit.net/) 中，允许你使用它来生成 MO 文件。在首选项中有一项设置，你可以启用或禁用它。

![国际化与本地化](https://i3.wp.com/developer.wordpress.org/files/2014/10/internationalization-localization-04.jpg)

### Grunt 任务

[grunt-po2mo](https://www.npmjs.com/package/grunt-po2mo) 可以转换所有文件。

## 优质翻译技巧

### 不要逐字翻译，要有机翻译

作为多语言使用者，你肯定知道不同语言在结构、节奏、语调和变化上各有差异。翻译信息无需与英文保持相同结构：理解原文表达的理念，用目标语言自然流畅的方式重新组织信息。这相当于创造对等信息而非相同信息：不要复制，而要替换。即使是信息中结构性较强的内容，如果认为调整后更符合逻辑或更适合目标受众，你也有权进行创造性改编。

### 尽量保持相同的正式程度（或非正式程度）

每条信息都有不同的正式或非正式程度。具体在目标语言中每条信息应使用何种程度的正式或非正式，需要您自行（或与团队共同）确定，但 WordPress 信息（尤其是通知类信息）在英语中通常采用礼貌的非正式语气。请尝试在您的文化背景下，在目标语言中实现同等效果。

### 避免使用俚语或特定受众术语

博客中难免会出现专业术语，但应避免使用只有"圈内人"才懂的俗语。如果一位新手博主用您的语言安装 WordPress，他们能理解这些术语的含义吗？像 pingback、trackback 和 feed 这类词汇是例外规则；这些术语通常难以翻译，许多译者选择保留英文原词。

### 阅读其他软件的本地化版本

如果你遇到困难或需要指导，可以尝试阅读其他流行软件工具的翻译版本，了解常用术语、正式程度等。当然，WordPress 有其独特的风格和感觉，在阅读其他本地化版本时请记住这一点，但可以自由挖掘 UI 术语等内容，以保持与你所用语言的其他软件的一致性。

## 使用本地化文件

将本地化文件放置在语言文件夹中，可以放在插件的 `languages` 文件夹内，或者从 WordPress 3.7 开始，通常放在 `wp-content` 下的插件 `languages` 文件夹中。完整路径为 `wp-content/languages/plugins/my-plugin-fr_FR.mo`。

您可以在“常规设置”中更改语言。如果看不到此选项，或者您想切换的语言未列出，请手动操作：

- 在 `wp-config.php` 中定义 `WPLANG` 为您选择的语言。例如，要使用法语，应添加：`define ('WPLANG', 'fr_FR');`
- 访问 `wp-admin/options-general.php` 或“设置” -> “常规”
- 在“站点语言”下拉菜单中选择您的语言
- 访问 `wp-admin/update-core.php`
- 点击“更新翻译”（如果可用）
- 核心翻译文件将被下载（如果可用）

## Resources

- [Creating .pot file for your theme or plugin](https://foxland.fi/creating-pot-file-for-your-theme-or-plugin/)
- [How To Internationalize WordPress Plugins](https://tommcfarlin.com/internationalize-wordpress-plugins/)
- [Translating Your Theme](https://code.tutsplus.com/translating-your-theme--wp-25014t)
- [Blank WordPress POT](https://github.com/fxbenard/Blank-WordPress-Pot)
- [Improved i18n WordPress tools](https://github.com/grappler/i18n)
- [How to update translations quickly](https://ulrich.pogson.ch/update-translations-quickly)
- [Gist: Complete Localization Grunt task](https://gist.github.com/grappler/10187003)
- WordPress.tv
  - [i18n](https://wordpress.tv/tag/i18n/)
  - [internationalization](https://wordpress.tv/tag/internationalization/)
  - [translation](https://wordpress.tv/tag/translation/)

# Index

国际化安全
-----------------------------

在讨论国际化时，安全问题常被忽视，但有几个重要事项需要牢记。

## 检查垃圾信息和恶意字符串

当译者向您提交本地化内容时，务必检查确认其翻译中未包含垃圾信息或其他恶意词汇。您可以使用 [Google 翻译](https://translate.google.com/) 将译文回译至您的母语，以便轻松比对原文与翻译后的字符串。

## 转义国际化字符串

不能假设译者仅会在本地化中添加无害文本；若其有意为之，可能会添加恶意 JavaScript 或其他代码。为防止此类情况，必须像对待其他不可信输入一样处理国际化字符串。

若需输出这些字符串，则应进行转义处理。

**不安全示例**

```
_e( 'The REST API content endpoints were added in WordPress 4.7.', 'your-text-domain' );
```

**安全示例**

```
esc_html_e( 'The REST API content endpoints were added in WordPress 4.7.', 'your-text-domain' );
```

另一种方案是选择依赖翻译验证机制，而非在代码中添加转义。验证机制的典型范例是 WordPress 多语言团队在 [translate.wordpress.org](https://translate.wordpress.org/) 中使用的[编辑者角色](https://make.wordpress.org/polyglots/handbook/glossary/#project-translation-editor)。该机制确保任何由不可信贡献者提交的翻译，在获准前均经过可信编辑者的验证。

## 对 URL 使用占位符

请勿在国际化字符串中包含 URL，因为恶意译者可能将其更改为指向其他网址。应使用 [`printf()`](https://www.php.net/manual/en/function.printf.php) 或 [`sprintf()`](https://www.php.net/manual/en/function.sprintf.php) 的占位符替代。

**不安全示例**

```
_e(
  'Please <a href="https://login.wordpress.org/register"> register for a WordPress.org account</a>.',
  'your-text-domain'
);
```

**安全示例**

```
printf(
  esc_html__( 'Please %1$s register for a WordPress.org account %2$s.', 'your-text-domain' ),
  '<a href="https://login.wordpress.org/register">',
  '</a>'
);
```

## 自行编译 .mo 二进制文件

译者通常会同时提交编译后的 .mo 文件与纯文本 .po 文件，但您应弃用其提供的 .mo 文件并自行编译。因为无法确认该文件是否由对应的 .po 文件编译而来，抑或是基于其他文件编译。若基于不同文件编译，则可能在不被察觉的情况下包含垃圾信息或恶意字符串。

使用 PoEdit 生成二进制文件会覆盖 .po 文件中的头部信息，因此更推荐通过命令行编译：

```
msgfmt -cv -o /path/to/output.mo /path/to/input.po
```

# 插件开发入门

欢迎阅读《插件开发者手册》。无论您是在编写第一个插件还是第五十个插件，我们都希望这份资源能帮助您编写出最优秀的插件。

本手册涵盖多种主题——从插件头部应包含的内容，到安全最佳实践，再到可用于构建插件的工具。它也是一份持续完善的文档——如果您发现任何缺失或不完整之处，请在 Slack 中通知文档团队，我们将共同改进。

## 为何开发插件

WordPress 开发中有一条核心原则：**不要修改 WordPress 核心代码**。这意味着您不应通过编辑核心文件来为网站添加功能，因为每次更新时 WordPress 都会覆盖核心文件。任何您想要添加或修改的功能都应通过插件实现。

根据您的需求，WordPress 插件可以非常简单，也可以非常复杂。最简单的插件是单个 PHP 文件。[Hello Dolly](https://wordpress.org/plugins/hello-dolly/ "Hello Dolly 插件") 就是这类插件的典型示例。插件 PHP 文件仅需包含[插件头部信息](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/)、若干 PHP 函数以及用于挂载函数的[钩子](https://developer.wordpress.org/plugins/hooks/)。

插件让您能够在不触及 WordPress 核心代码的前提下，极大地扩展其功能。

# 什么是插件？

插件是用于扩展 WordPress 核心功能的代码包。WordPress 插件由 PHP 代码构成，并可包含其他资源，如图像、CSS 和 JavaScript。

通过创建自己的插件，你正在 _扩展_ WordPress，例如，在 WordPress 已有功能之上构建额外功能，或者编写一个显示网站最近十篇文章链接的插件。

或者，利用 WordPress 的自定义文章类型，你可以编写一个创建功能完整的支持工单系统的插件，包含邮件通知、自定义工单状态和面向客户的入口。可能性是 _无限的_！

大多数 WordPress 插件由多个文件组成，但插件实际上 _只需要_ 一个主文件，其头部包含特定格式的 [DocBlock](https://en.wikipedia.org/wiki/PHPDoc)。

[Hello Dolly](https://wordpress.org/plugins/hello-dolly/ "Hello Dolly") 是最早的插件之一，仅 [100 行](https://plugins.trac.wordpress.org/browser/hello-dolly/trunk/hello.php) 代码。它在 WordPress 后台显示 [著名歌曲](https://en.wikipedia.org/wiki/Hello,_Dolly!_(song)) 的歌词。PHP 文件中使用了一些 CSS 来控制歌词的样式。

作为 WordPress.org 的插件作者，你拥有绝佳的机会来创建一个将被数百万 WordPress 用户安装、调整和喜爱的插件。**你** 只需要将你的好想法转化为代码。本插件手册将帮助你实现这一目标。

# JavaScript

JavaScript 是许多 WordPress 插件的重要组成部分。WordPress 核心捆绑了[多种 JavaScript 库](https://developer.wordpress.org/themes/basics/including-css-javascript/#default-scripts-included-and-registered-by-wordpress)。jQuery 是 WordPress 中最常用的库之一，因为它轻量且易于使用。您可以在插件中使用 jQuery 来操作 DOM 对象或执行 Ajax 操作。

# AJAX

## 什么是 AJAX？

AJAX 是 Asynchronous JavaScript And XML（异步 JavaScript 和 XML）的缩写。XML 是一种数据交换格式，而 UX 是开发者对用户体验的简称。Ajax 是一种互联网通信技术，允许用户浏览器中显示的网页向服务器请求特定信息，并在同一页面显示新信息，而无需重新加载整个页面。您已经可以想象这如何改善用户体验。

虽然 XML 是传统使用的数据交换格式，但实际交换可以采用任何方便的格式。在使用 PHP 代码时，许多开发者偏爱 JSON，因为从传输数据流创建的内部数据结构更易于对接。

要查看 AJAX 的实际效果，请进入您的 WordPress 管理区域并添加一个分类或标签。当您点击“添加新”按钮时请仔细观察，注意页面发生变化但并未实际重新加载。不相信吗？检查浏览器的后退历史记录，如果页面已重新加载，您会看到该页面的两个条目。

AJAX 甚至不需要用户操作即可工作。Google Docs 每隔几分钟就会通过 AJAX 自动保存您的文档，而无需您手动发起保存操作。

## 为何使用 AJAX？

显然，它能提升用户体验。AJAX 让你能够呈现动态、响应迅速且用户友好的体验，而非枯燥的静态页面。用户能即时获得操作正确与否的反馈，无需等到提交整个表单后才发现某个字段有误。重要字段可以在数据输入后立即验证，或在用户输入时提供建议。

AJAX 能显著减少前后端传输的数据量。只需交换相关数据，而非重新加载页面时传输的全部页面内容。

特别对于 WordPress 插件而言，AJAX 是目前实现独立于 WordPress 内容流程的最佳方式。如果你曾编写过 PHP 程序，可能会通过链接到新 PHP 页面来实现。用户点击链接即可触发流程。但问题在于，链接到外部 PHP 页面时无法调用任何 WordPress 函数。过去开发者通过在新 PHP 页面中包含核心文件 `wp-load.php` 来访问 WordPress 函数，但如今已无法确定该文件的正确路径。WordPress 架构现已足够灵活，`/wp-content/` 目录和插件文件可以从常规位置移动到安装根目录的下一级。你既无法确定 `wp-load.php` 相对于插件文件的位置，也无法获知安装文件夹的绝对路径。

而 AJAX 请求的发送位置是可知的，因为它定义在全局 JavaScript 变量中。你的 PHP AJAX 处理脚本实际上是一个动作钩子，因此与外部 PHP 文件不同，所有 WordPress 函数都能自动为其所用。

## 如何使用 AJAX？

如果你是 WordPress 新手，但在其他环境中使用过 AJAX，你需要重新学习一些知识。WordPress 实现 AJAX 的方式很可能与你习惯的方式不同。如果这对你来说都是全新的，也没关系。你将在这里学到基础知识。一旦你开发了一个基本的 AJAX 交互，在此基础上扩展并开发出具有出色用户界面的杀手级应用就轻而易举了！

WordPress 中的任何 AJAX 交互都有两个主要组成部分：客户端 JavaScript 或 jQuery，以及服务器端 PHP。所有 AJAX 交互都遵循以下事件序列。

1. 某种页面事件触发 JavaScript 或 jQuery 函数。该函数从页面收集一些数据，并通过 HTTP 请求将其发送到服务器。由于用 JavaScript 处理 HTTP 请求很麻烦，而且 WordPress 已经捆绑了 jQuery，因此从现在开始我们将只关注 jQuery 代码。使用纯 JavaScript 实现 AJAX 是可能的，但当 jQuery 可用时，这样做并不值得。
2. 服务器接收请求并对数据进行处理。它可能会组装相关数据，并以 HTTP 响应的形式将其发送回客户端浏览器。这不是必须的，但由于让用户了解正在发生的情况是可取的，因此不发送某种响应的情况非常罕见。
3. 发送初始 AJAX 请求的 jQuery 函数接收服务器响应并对其进行处理。它可能会更新页面上的某些内容，和/或通过某种方式向用户显示消息。

## 在 jQuery 中使用 AJAX

现在我们将定义[关于 jQuery 的文章片段](https://developer.wordpress.org/plugins/javascript/jquery/#selector-and-event)中的“执行操作”部分。我们将使用 [`$.post()`](https://api.jquery.com/jQuery.post/ "jQuery 参考") 方法，它接受 3 个参数：发送 POST 请求的 URL、要发送的数据以及处理服务器响应的回调函数。但在开始之前，我们需要做一些预先规划。我们进行以下赋值，以便稍后在回调函数中使用。其目的在[回调部分](https://developer.wordpress.org/plugins/javascript/ajax/#callback "页面章节")会更加明确。

### URL

所有 WordPress AJAX 请求都必须发送到 `wp-admin/admin-ajax.php`。正确且完整的 URL 必须通过 PHP 获取，jQuery 无法自行确定此值，并且您不能在 jQuery 代码中硬编码 URL 并期望其他人在其网站上使用您的插件。如果页面来自管理区域，WordPress 会在全局 JavaScript 变量 `ajaxurl` 中设置正确的 URL。对于公共区域的页面，您需要自行建立正确的 URL，并使用 [`wp_localize_script()`](https://developer.wordpress.org/reference/functions/wp_localize_script/) 将其传递给 jQuery。这将在 [PHP 部分](https://developer.wordpress.org/plugins/javascript/enqueuing/ "Server Side PHP and Enqueuing")中更详细地介绍。目前只需知道，适用于前端和后端的 URL 将作为您在 PHP 部分定义的全局对象的一个属性可用。在 jQuery 中，它像这样引用：

```
my_ajax_obj.ajax_url
```

### Data

All data that needs to be sent to the server is included in the data array. Besides any data needed by your app, you must send an action parameter. For requests that could result in a change to the database you need to send a nonce so the server knows the request came from a legitimate source. Our example data array provided to the `.post()` method looks like this:

```
{
  _ajax_nonce: my_ajax_obj.nonce, // nonce
  action: "my_tag_count", // action
  title: this.value // data
}
```

Each component is explained below.

### Nonce

[Nonce](https://developer.wordpress.org/apis/security/nonces/) 是 "Number used ONCE" 的合成词。它本质上是一个分配给每个表单实例的唯一序列号。Nonce 通过 PHP 脚本创建，并以与 URL 相同的方式传递给 jQuery，作为全局对象的一个属性。在此例中，它被引用为 my\_ajax\_obj.nonce。

[info]一个真正的 nonce 每次使用后都需要刷新，以便下一次 AJAX 调用有一个新的、未使用的 nonce 作为验证发送。实际上，WordPress 的 nonce 实现并非真正的 nonce。同一个 nonce 在 24 小时内可以多次使用，除非你登出。使用相同的种子短语生成 nonce 在 12 小时内总是产生相同的数字，之后才会生成新的数字。

如果你的应用需要严格的安全性，请实现一个真正的 nonce 系统，其中服务器在响应 Ajax 请求时发送一个新的 nonce，供脚本用于验证下一个请求。[/info]

最简单的方法是将此 nonce 值键设为 \_ajax\_nonce。如果与验证 nonce 的 PHP 代码协调一致，你也可以使用不同的键，但使用默认值更容易，无需担心协调问题。以下是此键值对的声明方式：

```
_ajax_nonce: my_ajax_obj.nonce
```

### 操作

所有 WordPress AJAX 请求都必须在数据中包含一个操作参数。该值是一个任意字符串，用于部分构建操作标签以挂载 AJAX 处理程序代码。建议此值能简要描述 AJAX 调用的用途。不出所料，该值的键名为 _‘action’_。在本示例中，我们将使用 `my_tag_count` 作为操作值。该键值对的声明如下所示：

```
action: "my_tag_count"
```

服务器执行任务所需的其他数据也包含在此数组中。如果需要传输大量字段，通常有两种格式可将数据字段合并为单个字符串以便传输：XML 和 JSON。使用这些格式是可选的，但无论采用何种方式，都需要与服务器端的 PHP 脚本协调配合。有关这些格式的更多信息，请参阅后续的回调部分。接收这种格式的数据比发送更常见，但两种方式均可使用。

在我们的示例中，服务器只需要一个值，即所选书名的单个字符串，因此我们将使用键名 _‘title’_。在 jQuery 中，触发事件的对象始终包含在变量 this 中。因此，所选元素的值为 this.value。该键值对的声明如下所示：

```
title: this.value
```

### 回调函数

回调处理程序是在请求发送后，服务器返回响应时执行的函数。通常我们在这里会看到一个匿名函数。该函数接收一个参数，即服务器响应。响应内容可以是简单的"是"或"否"，也可以是庞大的XML数据库。JSON格式数据也是常用的数据格式。响应甚至不是必需的——如果没有响应，则无需指定回调函数。为了用户体验考虑，最好让用户知道请求的处理结果，因此建议始终提供响应并给出操作已执行的提示。

在我们的示例中，我们用服务器响应替换了单选按钮后的当前文本，该响应包含按书名标记的帖子数量。以下是我们的匿名回调函数：

```
function( data ) {
  this2.nextSibling.remove();
  $( this2 ).after( data );
}
```

`data` 包含完整的服务器响应。之前我们通过 `var this2 = this;` 将触发变更事件的对象（引用为 `this`）赋值给 `this2`。这是因为闭包中的变量作用域仅延伸一层。通过在事件处理程序（最初仅包含 `/* do stuff */` 的部分）中赋值 `this2`，我们能够在回调函数中使用它，而在此处直接使用 `this` 会超出作用域范围。

服务器响应可以采用任何形式。大量数据应编码为数据流以便处理。XML和JSON是两种常见的编码方案。

#### XML

XML 是 AJAX 的旧式数据交换格式，毕竟 AJAX 中的 "X" 就代表它。尽管使用原生 PHP 函数处理 XML 可能较为困难，但它仍是一种可行的交换格式。正因如此，许多 PHP 程序员更偏好 JSON 交换格式。若使用 XML，解析方法取决于所用浏览器：Internet Explorer 使用 Microsoft.XMLDOM ActiveX，其他浏览器则使用 DOMParser。请注意 [WordPress 自 5.8 版本起已不再支持 Internet Explorer](https://make.wordpress.org/core/2021/04/22/ie-11-support-phase-out-plan/)。

#### JSON

JSON 因其轻量和易用性而备受青睐。实际上你可以使用 `eval()` 解析 JSON，但千万别这么做！使用 `eval()` 会带来严重的安全风险。请改用专用解析器，这样也更高效。使用全局解析器对象 `JSON` 的实例。[特定函数可提供简便方式](https://developer.wordpress.org/plugins/javascript/enqueuing/#json)，让你的 AJAX 调用获得 JSON 格式的响应。

#### Other

As long as the data format is coordinated with the PHP handler, it can be any format you like, such as comma delimited, tab delimited, or any kind of structure that works for you.

#### Client Side Summary

Now that we've added our callback as the final parameter for the $.post() function, we've completed our sample jQuery Ajax script. All the pieces put together look like this:

```
jQuery(document).ready(function($) {         //wrapper
  $(".pref").change(function() {          //event
    var this2 = this;                  //use in callback
    $.post(my_ajax_obj.ajax_url, {      //POST request
      _ajax_nonce: my_ajax_obj.nonce, //nonce
      action: "my_tag_count",         //action
      title: this.value               //data
      }, function(data) {            //callback
        this2.nextSibling.remove(); //remove current title
        $(this2).after(data);       //insert server response
      }
    );
  } );
} );
```

This script can either be output into a `block` on the web page or contained in its own file. This file can reside anywhere on the Internet, but most plugin developers place it in a `/js/` subfolder of the plugin's main folder. Unless you have reason to do otherwise, you may as well follow convention. For this example we will name our file `myjquery.js`.

# 服务器端 PHP 与脚本入队

实现 AJAX 通信所需的服务器端 PHP 脚本包含两个部分。首先需要在网页中入队 jQuery 脚本，并本地化 jQuery 脚本所需的 PHP 值。其次是实际处理 AJAX 请求的部分。

## 脚本入队

本节涵盖 WordPress 中 AJAX 的两个主要特性，常使经验丰富但不熟悉 WordPress 的开发者困惑。一是需要将脚本入队，以便元链接正确显示在页面头部区域。二是**所有** AJAX 请求都必须通过 `wp-admin/admin-ajax.php` 发送，切勿直接向插件页面发送请求。

### 入队

使用函数 [`wp_enqueue_script()`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) 让 WordPress 在页面的 `<head>` 部分插入指向你脚本的链接。切勿在头部模板中硬编码此类链接。作为插件开发者，你可能无法直接访问头部模板，但这条规则仍然值得强调。

入队函数接受五个参数如下：

- `$handle` 是脚本的名称。
- `$src` 定义脚本的位置。为了可移植性，请使用 `plugins_url()` 构建正确的 URL。如果你入队的脚本不是用于插件，请使用相关函数创建正确的 URL——切勿硬编码。
- `$deps` 是一个数组，可以处理你的新脚本所依赖的任何脚本，例如 jQuery。由于我们使用 jQuery 发送 AJAX 请求，你至少需要在数组中列出 `'jquery'`。
- `$ver` 允许你列出版本号。
- `$args` 是一个参数数组，用于定义页脚打印（通过 `in_footer` 键）和脚本加载策略（通过 `strategy` 键），例如 `defer` 或 `async`。从 WordPress 6.3 版本开始，这取代/重载了 `$in_footer` 参数。

```
wp_enqueue_script(
  'ajax-script',
  plugins_url( '/js/myjquery.js', __FILE__ ),
  array( 'jquery' ),
  '1.0.0',
  array(
    'in_footer' => true,
  )
);
```

你不能在插件代码页面加载时直接入队脚本。脚本必须通过几个动作钩子之一入队——具体使用哪个钩子取决于脚本需要链接到哪种页面。对于管理页面，使用 `admin_enqueue_scripts`。对于前端页面，使用 `wp_enqueue_scripts`，但登录页面除外，此时应使用 `login_enqueue_scripts`。

`admin_enqueue_scripts` 钩子会将当前页面文件名传递给回调函数。利用此信息，仅在需要的页面上入队你的脚本。前端版本不传递任何参数。在这种情况下，使用模板标签如 `is_home()`、`is_single()` 等，以确保仅在需要的地方入队脚本。这是我们示例的完整入队代码：

```
add_action( 'admin_enqueue_scripts', 'my_enqueue' );
function my_enqueue( $hook ) {
  if ( 'myplugin_settings.php' !== $hook ) {
    return;
  }
  wp_enqueue_script(
    'ajax-script',
    plugins_url( '/js/myjquery.js', __FILE__ ),
    array( 'jquery' ),
    '1.0.0',
    array(
      'in_footer' => true,
    )
  );
}
```

为什么我们在这里使用命名函数，而在 jQuery 中使用匿名函数？因为闭包直到最近才被 PHP 支持。jQuery 支持闭包已经有一段时间了。由于有些人可能仍在运行旧版本的 PHP，我们总是使用命名函数以确保最大兼容性。如果你使用的是较新的 PHP 版本，并且只为你自己的安装进行开发，那么可以随意使用闭包。

#### 注册与入队

您会在其他教程中看到严格使用 [`wp_register_script()`](https://developer.wordpress.org/reference/functions/wp_register_script/) 的示例。这没问题，但它的使用是可选的。而 `wp_enqueue_script()` 则是必需的。必须调用此函数，您的脚本文件才能正确链接到网页上。那么为什么要注册脚本呢？它会创建一个有用的标签或句柄，您可以根据需要在代码的各个部分轻松引用该脚本。如果您只需要加载脚本且不在代码的其他地方引用它，则无需注册。

#### 延迟脚本加载

WordPress 通过 `wp_register_script()` 和 `wp_enqueue_script()` 函数提供对脚本加载策略的支持，这是通过 WordPress 6.3 中引入的新 `$args` 数组参数中的 `strategy` 键实现的。

支持的策略如下：

- **defer**
  - 通过向 `$args` 参数添加数组键值对 `'strategy' => 'defer'` 来指定。
  - 标记为延迟执行的脚本（通过 `defer` 脚本属性）仅在 DOM 树完全加载后执行（但在 `DOMContentLoaded` 和窗口加载事件之前）。与异步脚本不同，延迟脚本按照它们在 DOM 中打印/添加的顺序执行。
- **async**
  - 通过向 `$args` 参数添加数组键值对 `'strategy' => 'async'` 来指定。
  - 标记为异步执行的脚本（通过 `async` 脚本属性）在浏览器加载完成后立即执行。异步脚本没有保证的执行顺序，因为脚本 B（尽管在脚本 A 之后添加到 DOM 中）可能先执行，前提是它可能在脚本 A 之前完成加载。此类脚本可能在 DOM 完全构建之前或 `DOMContentLoaded` 事件之后执行。

以下是在我们的插件中为额外脚本入队指定加载策略的示例：

```
wp_register_script(
  'ajax-script-two',
  plugins_url( '/js/myscript.js', __FILE__ ),
  array( ajax-script ),
  '1.0.,0',
  array(
    'strategy' => 'defer',
  )
);
```

使用 `wp_enqueue_script()` 时也适用相同的方法。在上面的示例中，我们表明我们打算以延迟方式加载 `'ajax-script-two'` 脚本。

在指定延迟脚本加载策略时，系统会考虑脚本的依赖关系树（其依赖项和/或被依赖项）来决定“合格策略”，以避免应用对某个脚本有效但对树中其他脚本有害的策略，从而导致意外的执行顺序错误。由于这种逻辑，您通过 `$args` 参数传递的预期加载策略可能不是最终（选定）的策略，但它永远不会比预期策略更有害（或更严格）。

### 随机数

你需要创建一个随机数，以便 jQuery AJAX 请求能够被验证为合法请求，而非来自未知恶意方的潜在有害请求。只有你的 PHP 脚本和 jQuery 脚本会知道这个值。当收到请求时，你可以验证它是否与此处创建的值相同。以下是为我们的示例创建随机数的方法：

```
$title_nonce = wp_create_nonce( 'title_example' );
```

参数 `title_example` 可以是任意字符串。建议该字符串与随机数的用途相关，但实际上可以是任何适合你的内容。

### 本地化

如果您还记得 [jQuery 章节](https://developer.wordpress.org/plugins/javascript/jquery/) 的内容，PHP 创建供 jQuery 使用的数据是通过一个名为 `my_ajax_obj` 的全局对象传递的。在我们的示例中，这些数据是一个随机数（nonce）以及 `admin-ajax.php` 的完整 URL。分配对象属性并创建全局 jQuery 对象的过程称为**本地化**。以下是我们示例中使用的本地化代码，它使用了 [`wp_localize_script()`](https://developer.wordpress.org/reference/functions/wp_localize_script/) 函数。

```
wp_localize_script(
  'ajax-script',
  'my_ajax_obj',
  array(
    'ajax_url' => admin_url( 'admin-ajax.php' ),
    'nonce'    => $title_nonce,
  )
);
```

请注意，我们使用了脚本句柄 `ajax-script`，以确保全局对象被分配给正确的脚本。该对象仅对我们的脚本是全局的，而不是对所有脚本。本地化也可以从用于入队脚本的同一个钩子中调用。创建随机数也是如此，尽管该特定函数几乎可以在任何地方调用。所有这些组合在一个钩子回调函数中，看起来像这样：

```
add_action( 'admin_enqueue_scripts', 'my_enqueue' );

/**
 * 入队我的脚本和资源。
 *
 * @param $hook
 */
function my_enqueue( $hook ) {
  if ( 'myplugin_settings.php' !== $hook ) {
    return;
  }
  wp_enqueue_script(
    'ajax-script',
    plugins_url( '/js/myjquery.js', __FILE__ ),
    array( 'jquery' ),
    '1.0.0',
    true
  );

  wp_localize_script(
    'ajax-script',
    'my_ajax_obj',
    array(
      'ajax_url' => admin_url( 'admin-ajax.php' ),
      'nonce'    => wp_create_nonce( 'title_example' ),
    )
  );
}
```

[info]请记住，仅将随机数本地化添加到需要的页面，不要向不应使用它的人显示随机数。并且记得使用 `current_user_can()` 配合权限或角色来完善安全性。[/info]

## AJAX Action

The other major part of the server side PHP code is the actual AJAX handler that receives the POSTed data, does something with it, then sends an appropriate response back to the browser. This takes on the form of a WordPress [action hook](https://developer.wordpress.org/plugins/hooks/actions/). Which hook tag you use depends on whether the user is logged in or not and what value your jQuery script passed as the _action:_ value.

[info]`$_GET`, `$_POST`, and `$_COOKIE` vs. `$_REQUEST`

You've probably used one or more of the PHP super globals such as `$_GET` or `$_POST` to retrieve values from forms or cookies (using `$_COOKIE`). Maybe you prefer `$_REQUEST` instead, or at least have seen it used. It's kind of cool – regardless of the request method, `POST` or `GET`, it will have the form values. Works great for pages that use both methods. On top of that, it has cookie values as well. One stop shopping! Therein lies its tragic flaw. In the case of a name conflict, the cookie value will override any form values. Thus it is ridiculously easy for a bad actor to craft a counterfeit cookie on their browser, which will overwrite any form value you might be expecting from the request. `$_REQUEST` is an easy route for hackers to inject arbitrary data into your form values. To be extra safe, stick to the specific variables and avoid the one size fits all.[/info]

Since our AJAX exchange is for the plugin's settings page, the user must be logged in. If you recall from the [jQuery section](https://developer.wordpress.org/plugins/javascript/jquery/), the `action:` value is `"my_tag_count"`. This means our action hook tag will be `wp_ajax_my_tag_count`. If our AJAX exchange were to be utilized by users who were not currently logged in, the action hook tag would be `wp_ajax_nopriv_my_tag_count` The basic code used to hook the action looks like this:

```
add_action( 'wp_ajax_my_tag_count', 'my_ajax_handler' );

/**
 * Handles my AJAX request.
 */
function my_ajax_handler() {
  // Handle the ajax request here

  wp_die(); // All ajax handlers die when finished
}
```

The first thing your AJAX handler should do is verify the nonce sent by jQuery with [`check_ajax_referer()`](https://developer.wordpress.org/reference/functions/check_ajax_referer/), which should be the same value that was localized when the script was enqueued.

```
check_ajax_referer( 'title_example' );
```

The provided parameter must be identical to the parameter provided [earlier](https://developer.wordpress.org/plugins/javascript/enqueuing/#php-nonce) to `wp_create_nonce()`. The function simply dies if the nonce does not check out. If this were a true nonce, now that it was used, the value is no longer any good. You would then generate a new one and send it to the callback script so that it can be used for the next request. But since WordPress nonces are good for twenty-four hours, you needn't do anything but check it.

### 数据

处理完 nonce 后，我们的处理器就可以处理 jQuery 脚本通过 `$_POST['title']` 发送的数据了。首先，我们通过 [`wp_unslash()`](https://developer.wordpress.org/reference/functions/wp_unslash/) 函数运行该值以移除任何意外的引号，然后将其赋值给一个新变量。

```
$title = wp_unslash( $_POST['title'] );
```

我们可以使用 [`update_user_meta()`](https://developer.wordpress.org/reference/functions/update_user_meta/) 将用户的选择保存到用户元数据中。

```
update_user_meta( get_current_user_id(), 'title_preference', sanitize_post_title( $title ) );
```

接着，我们构建一个查询以获取所选标题标签的文章数量。

```
$args      = array(
  'tag' => $title,
);
$the_query = new WP_Query( $args );
```

最后，我们可以将响应发送回 jQuery 脚本。传输数据有几种方式。在深入我们示例的具体细节之前，先来看看一些可选的方案。

#### XML

PHP 对 XML 的支持尚有不足。幸运的是，WordPress 提供了 [`WP_Ajax_Response`](https://developer.wordpress.org/reference/classes/wp_ajax_response/) 类来简化这一任务。该类会生成 XML 格式的响应，设置正确的头部内容类型，输出响应 XML 后终止执行，从而确保返回正确的 XML 响应。

#### JSON

这种格式轻量且易于使用，WordPress 提供了 [`wp_send_json`](https://developer.wordpress.org/reference/functions/wp_send_json/) 函数来对响应进行 JSON 编码、输出并终止执行——这有效地替代了 [`WP_Ajax_Response`](https://developer.wordpress.org/reference/classes/wp_ajax_response/)。WordPress 还提供了 [`wp_send_json_success`](https://developer.wordpress.org/reference/functions/wp_send_json_success/) 和 [`wp_send_json_error`](https://developer.wordpress.org/reference/functions/wp_send_json_error/) 函数，它们允许在 JavaScript 中触发相应的 done() 或 fail() 回调。

#### 其他

您可以使用任何喜欢的方式传输数据，只要发送方和接收方协调一致即可。逗号分隔或制表符分隔等文本格式只是众多可能性之一。对于少量数据，直接发送原始数据流可能就足够了。这就是我们将在示例中采用的方式——我们将仅发送实际的替换 HTML，不包含其他内容。

```
echo esc_html( $title ) . ' (' . $the_query->post_count . ') ';
```

在实际应用中，您必须考虑到操作可能因某些原因失败的可能性——例如，数据库服务器可能宕机。响应应考虑到这种意外情况，接收响应的 jQuery 脚本也应相应处理，例如告知用户稍后重试。

### 终止处理

当处理器完成所有任务后，需要终止运行。如果使用 [`WP_Ajax_Response`](https://developer.wordpress.org/reference/classes/wp_ajax_response/) 或 `wp_send_json*` 系列函数，系统会自动处理此步骤。否则，直接调用 WordPress 的 [`wp_die()`](https://developer.wordpress.org/reference/functions/wp_die/) 函数即可。

### AJAX Handler Summary

The complete AJAX handler for our example looks like this:

```
/**
 * AJAX handler using JSON
 */
function my_ajax_handler__json() {
  check_ajax_referer( 'title_example' );
  $title = wp_unslash( $_POST['title'] );

  update_user_meta( get_current_user_id(), 'title_preference', sanitize_post_title( $title ) );

  $args      = array(
    'tag' => $title,
  );
  $the_query = new WP_Query( $args );
  wp_send_json( esc_html( $title ) . ' (' . $the_query->post_count . ') ' );
}

/**
 * AJAX handler not using JSON.
 */
function my_ajax_handler() {
  check_ajax_referer( 'title_example' );
  $title = wp_unslash( $_POST['title'] );

  update_user_meta( get_current_user_id(), 'title_preference', sanitize_post_title( $title ) );

  $args      = array(
    'tag' => $title,
  );
  $the_query = new WP_Query( $args );
  echo esc_html( $title ) . ' (' . $the_query->post_count . ') ';
  wp_die(); // All ajax handlers should die when finished
}
```

# Heartbeat API

Heartbeat API 是 WordPress 内置的简易服务器轮询 API，可实现近乎实时的前端更新。

## 工作原理

页面加载时，客户端心跳代码会设置一个间隔（称为"tick"），每 15-120 秒运行一次。运行时，heartbeat 会收集数据并通过 jQuery 事件发送，然后将数据发送到服务器并等待响应。在服务器端，admin-ajax 处理器接收传递的数据，准备响应，过滤响应，最后以 JSON 格式返回数据。客户端接收数据后触发最终的 jQuery 事件，表示数据已接收。

自定义 Heartbeat 事件的基本流程：

1. 向待发送数据添加额外字段（JS `heartbeat-send` 事件）
2. 在 PHP 中检测发送字段，并添加响应字段（`heartbeat_received` 过滤器）
3. 在 JS 中处理返回数据（JS `heartbeat-tick` 事件）

（根据功能需求，可选择仅使用其中一或两个事件）

## Using the API

Using the heartbeat API requires two separate pieces of functionality: send and receive callbacks in JavaScript, and a server-side filter to process passed data in PHP.

### Sending Data to the Server

When Heartbeat sends data to the server, you can include custom data. This can be any data you want to send to the server, or a simple true value to indicate you are expecting data.

```
jQuery( document ).on( 'heartbeat-send', function ( event, data ) {
  // Add additional data to Heartbeat data.
  data.myplugin_customfield = 'some_data';
});
```

### Receiving and Responding on the Server

On the server side, you can then detect this data, and add additional data to the response.

```
/**
 * Receive Heartbeat data and respond.
 *
 * Processes data received via a Heartbeat request, and returns additional data to pass back to the front end.
 *
 * @param array $response Heartbeat response data to pass back to front end.
 * @param array $data     Data received from the front end (unslashed).
 *
 * @return array
 */
function myplugin_receive_heartbeat( array $response, array $data ) {
  // If we didn't receive our data, don't send any back.
  if ( empty( $data['myplugin_customfield'] ) ) {
    return $response;
  }

  // Calculate our data and pass it back. For this example, we'll hash it.
  $received_data = $data['myplugin_customfield'];

  $response['myplugin_customfield_hashed'] = sha1( $received_data );
  return $response;
}
add_filter( 'heartbeat_received', 'myplugin_receive_heartbeat', 10, 2 );
```

Back on the frontend, you can then handle receiving this data back.

```
jQuery( document ).on( 'heartbeat-tick', function ( event, data ) {
  // Check for our data, and use it.
  if ( ! data.myplugin_customfield_hashed ) {
    return;
  }

  alert( 'The hash is ' + data.myplugin_customfield_hashed );
});
```

Not every feature will need all three of these steps. For example, if you don’t need to send any data to the server, you can use just the latter two steps.

# jQuery

## 使用 jQuery

当用户浏览器接收到您的 WordPress 网页后，jQuery 脚本便开始运行。一个基本的 jQuery 语句包含两部分：选择器（决定代码应用于哪些 HTML 元素）和动作或事件（决定代码执行什么操作或响应什么）。基本的事件语句如下所示：

```
jQuery.(selector).event(function);
```

当选择器选中的 HTML 元素中发生事件（例如鼠标点击）时，最后一组括号内定义的函数将被执行。

以下所有代码示例均基于此 HTML 页面内容。假设它出现在您插件的管理设置屏幕上，由文件 `myplugin_settings.php` 定义。这是一个简单的表格，每个标题旁边都有单选按钮。

```
<form id="radioform">
  <table>
    <tbody>
    <tr>
      <td><input class="pref" checked="checked" name="book" type="radio" value="Sycamore Row" />Sycamore Row</td>
      <td>John Grisham</td>
    </tr>
    <tr>
      <td><input class="pref" name="book" type="radio" value="Dark Witch" />Dark Witch</td>
      <td>Nora Roberts</td>
    </tr>
    </tbody>
  </table>
</form>
```

在您的设置页面上，输出可能类似这样。

![示例表格](https://i3.wp.com/make.wordpress.org/docs/files/2013/11/pdh-ajax-example.png)

在 [AJAX 文章](https://developer.wordpress.org/plugins/javascript/ajax/)中，我们将构建一个 AJAX 交互，将用户选择保存在用户元数据中，并添加带有选定标题标签的文章数量。这不是一个非常实用的应用，但它说明了所有重要步骤。jQuery 代码可以驻留在外部文件中，也可以输出到页面内的 `<script>` 块中。我们将重点讨论外部文件变体，因为从 PHP 传递值需要特别注意。如果您认为更便捷，相同的代码也可以输出到页面中。

### Selector and Event

The selector is the same form as CSS selectors: `.class` or `#id`. There's many [more forms](https://api.jquery.com/category/selectors/ "jQuery Reference"), but these are the two you will frequently use. In our example, we will use class `.pref`. There's also a slew of possible [events](https://api.jquery.com/category/events/ "jQuery Reference"), one you will likely use a lot is _‘click'_. In our example we will use _‘change'_ to capture a radio button selection. Be aware that jQuery events are often named somewhat differently than those with JavaScript. So far, after we add in an empty anonymous function, our example statement looks like this:

```
$.(".pref").change(function(){
  /*do stuff*/
});
```

This code will "do stuff" when any element of the "pref" class changes.

[info]This code snippet, and all examples on this page, are for illustrating the use of AJAX. The code is not suitable for production environments because related operations such as [sanitization](https://developer.wordpress.org/plugins/security/securing-input/), [security](https://developer.wordpress.org/apis/security/nonces/), [error handling](https://www.sitepoint.com/error-handling-in-php/), and [internationalization](https://developer.wordpress.org/plugins/internationalization/) have been intentionally omitted. Be sure to always address these important operations in your production code.[/info]

# Summary

Here are all the example code snippets from the preceding discussion, assembled into two complete code pages: one for jQuery and the other for PHP.

## PHP

This code resides on one of your plugin pages.

```
add_action( 'admin_enqueue_scripts', 'my_enqueue' );
function my_enqueue( $hook ) {
  if ( 'myplugin_settings.php' !== $hook ) {
    return;
  }

  wp_enqueue_script(
    'ajax-script',
    plugins_url( '/js/myjquery.js', __FILE__ ),
    array( 'jquery' ),
    '1.0.0',
    true
  );

  $title_nonce = wp_create_nonce( 'title_example' );
  wp_localize_script(
    'ajax-script',
    'my_ajax_obj',
    array(
      'ajax_url' => admin_url( 'admin-ajax.php' ),
      'nonce'    => $title_nonce,
    )
  );
}

add_action( 'wp_ajax_my_tag_count', 'my_ajax_handler' );
function my_ajax_handler() {
  check_ajax_referer( 'title_example' );

  $title = wp_unslash( $_POST['title'] );

  update_user_meta( get_current_user_id(), 'title_preference', $title );

  $args = array(
    'tag' => $title,
  );

  $the_query = new WP_Query( $args );

  echo esc_html( $title ) . ' (' . $the_query->post_count . ') ';

  wp_die(); // all ajax handlers should die when finished
}
```

## jQuery

This code is in the file `js/myjquery.js` below your plugin folder.

```
jQuery(document).ready(function($) { 	   //wrapper
  $(".pref").change(function() { 		   //event
    var this2 = this; 		           //use in callback
    $.post(my_ajax_obj.ajax_url, { 	   //POST request
      _ajax_nonce: my_ajax_obj.nonce, //nonce
      action: "my_tag_count",        //action
      title: this.value 	           //data
    }, function(data) {		           //callback
      this2.nextSibling.remove();    //remove the current title
      $(this2).after(data); 	       //insert server response
    });
  });
});
```

And after storing the preference, the resulting post count is added to the selected title.

## More Information

- [How To Use AJAX In WordPress](https://www.smashingmagazine.com/2011/10/how-to-use-ajax-in-wordpress/)
- [AJAX for WordPress](https://glennmessersmith.com/pages/wpajax.html)

# 元数据

元数据是关于信息的信息。在 WordPress 中，它指的是与文章、用户、评论和分类相关联的信息。

鉴于 WordPress 中元数据存在多对一的关系，您的选择几乎是无限的。您可以拥有任意数量的元选项，并且几乎可以在其中存储任何内容。

本章将讨论[管理文章元数据](https://developer.wordpress.org/plugins/metadata/managing-post-metadata/)、[创建自定义元框](https://developer.wordpress.org/plugins/metadata/custom-meta-boxes/)以及[渲染文章元数据](https://developer.wordpress.org/plugins/metadata/rendering-post-metadata/)。

# 自定义元框

## 什么是元框？

当用户编辑文章时，编辑界面由多个默认框组成：编辑器、发布、分类、标签等。这些框就是元框。插件可以向任何文章类型的编辑界面添加自定义元框。

自定义元框的内容通常是 HTML 表单元素，用户在其中输入与插件功能相关的数据，但其内容实际上可以是您需要的任何 HTML。

## 为何使用元数据框？

元数据框是便捷、灵活、模块化的编辑屏幕元素，可用于收集与正在编辑的文章相关的信息。您的自定义元数据框将与所有其他文章相关信息显示在同一屏幕上，从而建立清晰的关系。

元数据框可以轻松对不需要查看的用户隐藏，而对需要查看的用户显示。用户可以在编辑屏幕上自由排列元数据框，从而控制其编辑环境。

[alert]本页所有示例仅用于说明目的。代码不适用于生产环境。

诸如[输入安全处理](https://developer.wordpress.org/apis/security/sanitizing/)、[用户权限](https://developer.wordpress.org/plugins/security/checking-user-capabilities/)、[随机数](https://developer.wordpress.org/apis/security/nonces/)和[国际化](https://developer.wordpress.org/plugins/internationalization/)等操作已被有意省略。请务必始终处理这些重要操作。[/alert]

## Adding Meta Boxes

To create a meta box use the [`add_meta_box()`](https://developer.wordpress.org/reference/functions/add_meta_box/) function and plug its execution to the [`add_meta_boxes`](https://developer.wordpress.org/reference/hooks/add_meta_boxes/) action hook.

The following example is adding a meta box to the `post` edit screen and the `wporg_cpt` edit screen.

```
function wporg_add_custom_box() {
  $screens = [ 'post', 'wporg_cpt' ];
  foreach ( $screens as $screen ) {
    add_meta_box(
      'wporg_box_id',                 // Unique ID
      'Custom Meta Box Title',      // Box title
      'wporg_custom_box_html',  // Content callback, must be of type callable
      $screen                            // Post type
    );
  }
}
add_action( 'add_meta_boxes', 'wporg_add_custom_box' );
```

The `wporg_custom_box_html` function will hold the HTML for the meta box.

The following example is adding form elements, labels, and other HTML elements.

```
function wporg_custom_box_html( $post ) {
  ?>
  <label for="wporg_field">Description for this field</label>
  <select name="wporg_field" id="wporg_field" class="postbox">
    <option value="">Select something...</option>
    <option value="something">Something</option>
    <option value="else">Else</option>
  </select>
  <?php
}
```

[info]**Note there are no submit buttons in meta boxes.** The meta box HTML is included inside the edit screen's form tags, all the post data including meta box values are transfered via `POST` when the user clicks on the Publish or Update buttons.[/info]

The example shown here only contains one form field, a drop down list. You may create as many as needed in any particular meta box. If you have a lot of fields to display, consider using multiple meta boxes, grouping similar fields together in each meta box. This helps keep the page more organized and visually appealing.

### Getting Values

To retrieve saved user data and make use of it, you need to get it from wherever you saved it initially. If it was stored in the `postmeta` table, you may get the data with [`get_post_meta()`](https://developer.wordpress.org/reference/functions/get_post_meta/).

The following example enhances the previous form elements with pre-populated data based on saved meta box values. You will learn how to save meta box values in the next section.

```
function wporg_custom_box_html( $post ) {
  $value = get_post_meta( $post->ID, '_wporg_meta_key', true );
  ?>
  <label for="wporg_field">Description for this field</label>
    <select name="wporg_field" id="wporg_field" class="postbox">
      <option value="">Select something...</option>
      <option value="something" <?php selected( $value, 'something' ); ?>>Something</option>
      <option value="else" <?php selected( $value, 'else' ); ?>>Else</option>
    </select>
    <?php
}
```

More on the [`selected()`](https://developer.wordpress.org/reference/functions/selected/) function.

### 保存数值

当文章类型被保存或更新时，会触发多个操作，其中任何一个都可能适合挂钩以保存输入的值。在本示例中，我们使用 `save_post` 动作钩子，但在某些情况下，其他钩子可能更合适。请注意，对于单个更新事件，`save_post` 可能会触发多次。请相应地构建保存数据的方法。

您可以将输入的数据保存在任何地方，甚至可以在 WordPress 之外。由于您可能处理与文章相关的数据，`postmeta` 表通常是存储数据的好地方。

以下示例将把 `wporg_field` 字段值保存在隐藏的元键 `_wporg_meta_key` 中。

```
function wporg_save_postdata( $post_id ) {
  if ( array_key_exists( 'wporg_field', $_POST ) ) {
    update_post_meta(
      $post_id,
      '_wporg_meta_key',
      $_POST['wporg_field']
    );
  }
}
add_action( 'save_post', 'wporg_save_postdata' );
```

在生产代码中，请记住遵循信息框中概述的安全措施！

## 幕后机制

通常您无需关心幕后的运行机制。本节内容是为了保持文档完整性而添加。

当文章编辑界面需要显示所有已添加的元数据框时，它会调用 [`do_meta_boxes()`](https://developer.wordpress.org/reference/functions/do_meta_boxes/) 函数。该函数会遍历所有元数据框，并触发每个框关联的 `callback` 回调函数。

在每次调用之间，系统会插入相应的标记元素（例如 div 容器、标题等）。

## 移除元框

要从编辑屏幕移除现有的元框，请使用 [`remove_meta_box()`](https://developer.wordpress.org/reference/functions/remove_meta_box/) 函数。传递的参数必须与使用 [`add_meta_box()`](https://developer.wordpress.org/reference/functions/add_meta_box/) 添加元框时所用的参数完全匹配。

要移除默认的元框，请检查源代码中使用的参数。默认的 [`add_meta_box()`](https://developer.wordpress.org/reference/functions/add_meta_box/) 调用位于 `wp-includes/edit-form-advanced.php` 文件中。

## Implementation Variants

So far we've been using the procedural technique of implementing meta boxes. Many plugin developers find the need to implement meta boxes using various other techniques.

### OOP

Adding meta boxes using OOP is easy and saves you from having to worry about naming collisions in the global namespace.

To save memory and allow easier implementation, the following example uses an abstract Class with static methods.

```
abstract class WPOrg_Meta_Box {

  /**
   * Set up and add the meta box.
   */
  public static function add() {
    $screens = [ 'post', 'wporg_cpt' ];
    foreach ( $screens as $screen ) {
      add_meta_box(
        'wporg_box_id',          // Unique ID
        'Custom Meta Box Title', // Box title
        [ self::class, 'html' ],   // Content callback, must be of type callable
        $screen                  // Post type
      );
    }
  }

  /**
   * Save the meta box selections.
   *
   * @param int $post_id  The post ID.
   */
  public static function save( int $post_id ) {
    if ( array_key_exists( 'wporg_field', $_POST ) ) {
      update_post_meta(
        $post_id,
        '_wporg_meta_key',
        $_POST['wporg_field']
      );
    }
  }

  /**
   * Display the meta box HTML to the user.
   *
   * @param WP_Post $post   Post object.
   */
  public static function html( $post ) {
    $value = get_post_meta( $post->ID, '_wporg_meta_key', true );
    ?>
    <label for="wporg_field">Description for this field</label>
    <select name="wporg_field" id="wporg_field" class="postbox">
      <option value="">Select something...</option>
      <option value="something" <?php selected( $value, 'something' ); ?>>Something</option>
      <option value="else" <?php selected( $value, 'else' ); ?>>Else</option>
    </select>
    <?php
  }
}

add_action( 'add_meta_boxes', [ 'WPOrg_Meta_Box', 'add' ] );
add_action( 'save_post', [ 'WPOrg_Meta_Box', 'save' ] );
```

### AJAX

由于元框的 HTML 元素位于编辑屏幕的 `form` 标签内，默认行为是在用户提交页面后从 `$_POST` 超级全局变量中解析元框值。

您可以使用 AJAX 增强默认体验；这允许根据用户输入和行为执行操作，无论他们是否已提交页面。

#### Define a Trigger

First, you must define the trigger, this can be a link click, a change of a value or any other JavaScript event.

In the example below we will define `change` as our trigger for performing an AJAX request.

```
/*jslint browser: true, plusplus: true */
(function ($, window, document) {
  'use strict';
  // execute when the DOM is ready
  $(document).ready(function () {
    // js 'change' event triggered on the wporg_field form field
    $('#wporg_field').on('change', function () {
      // our code
    });
  });
}(jQuery, window, document));
```

#### Client Side Code

Next, we need to define what we want the trigger to do, in other words we need to write our client side code.

In the example below we will make a `POST` request, the response will either be success or failure, this will indicate wither the value of the `wporg_field` is valid.

```
/*jslint browser: true, plusplus: true */
(function ($, window, document) {
  'use strict';
  // execute when the DOM is ready
  $(document).ready(function () {
    // js 'change' event triggered on the wporg_field form field
    $('#wporg_field').on('change', function () {
      // jQuery post method, a shorthand for $.ajax with POST
      $.post(wporg_meta_box_obj.url,                        // or ajaxurl
        {
          action: 'wporg_ajax_change',                // POST data, action
          wporg_field_value: $('#wporg_field').val(), // POST data, wporg_field_value
          post_ID: jQuery('#post_ID').val()           // The ID of the post currently being edited
        }, function (data) {
          // handle response data
          if (data === 'success') {
            // perform our success logic
          } else if (data === 'failure') {
            // perform our failure logic
          } else {
            // do nothing
          }
        }
      );
    });
  });
}(jQuery, window, document));
```

We took the WordPress AJAX file URL dynamically from the `wporg_meta_box_obj` JavaScript custom object that we will create in the next step.

[info]If your meta box only requires the WordPress AJAX file URL; instead of creating a new custom JavaScript object you could use the `ajaxurl` predefined JavaScript variable.

**Available only in the WordPress Administration.** Make sure it is not empty before performing any logic.[/info]

#### Enqueue Client Side Code

Next step is to put our code into a script file and enqueue it on our edit screens.

In the example below we will add the AJAX functionality to the edit screens of the following post types: post, `wporg_cpt`.

The script file will reside at `/plugin-name/admin/meta-boxes/js/admin.js`, `plugin-name` being the main plugin folder, `/plugin-name/plugin.php` the file calling the function.

```
function wporg_meta_box_scripts() {
  // get current admin screen, or null
  $screen = get_current_screen();
  // verify admin screen object
  if (is_object($screen)) {
    // enqueue only for specific post types
    if (in_array($screen->post_type, ['post', 'wporg_cpt'])) {
      // enqueue script
      wp_enqueue_script('wporg_meta_box_script', plugin_dir_url(__FILE__) . 'admin/meta-boxes/js/admin.js', ['jquery']);
      // localize script, create a custom js object
      wp_localize_script(
        'wporg_meta_box_script',
        'wporg_meta_box_obj',
        [
          'url' => admin_url('admin-ajax.php'),
        ]
      );
    }
  }
}
add_action('admin_enqueue_scripts', 'wporg_meta_box_scripts');
```

#### Server Side Code

The last step is to write our server side code that is going to handle the request.

```
// The piece after `wp_ajax_`  matches the action argument being sent in the POST request.
add_action( 'wp_ajax_wporg_ajax_change', 'my_ajax_handler' );

/**
 * Handles my AJAX request.
 */
function my_ajax_handler() {
  // Handle the ajax request here
  if ( array_key_exists( 'wporg_field_value', $_POST ) ) {
    $post_id = (int) $_POST['post_ID'];
    if ( current_user_can( 'edit_post', $post_id ) ) {
      update_post_meta(
        $post_id,
        '_wporg_meta_key',
        $_POST['wporg_field_value']
      );
    }
  }

  wp_die(); // All ajax handlers die when finished
}
```

As a final reminder, the code illustrated on this page lacks important operations that take care of security. Be sure your production code includes such operations.

See [Handbook's AJAX Chapter](https://developer.wordpress.org/plugins/javascript/ajax/) and the [Codex](https://codex.wordpress.org/AJAX_in_Plugins) for more on AJAX.

## More Information

- [Complex Meta Boxes in WordPress](https://www.wproots.com/complex-meta-boxes-in-wordpress/)
- [How To Create Custom Post Meta Boxes In WordPress](https://www.smashingmagazine.com/2011/10/create-custom-post-meta-boxes-wordpress/)
- [WordPress Meta Boxes: a Comprehensive Developer's Guide](https://themefoundation.com/wordpress-meta-boxes-guide/)

# 管理文章元数据

## 添加元数据

使用 [`add_post_meta()`](https://developer.wordpress.org/reference/functions/add_post_meta/) 可以非常轻松地添加元数据。该函数接受 `post_id`、`meta_key`、`meta_value` 和一个 `unique` 标志。

`meta_key` 是您的插件在代码其他地方引用元数据值的方式。像 `mycrazymetakeyname` 这样的名称也可以使用，但使用与您的插件或主题相关的前缀，后跟键的描述会更实用。例如 `wporg_featured_menu` 可能是一个不错的选择。需要注意的是，同一个 `meta_key` 可以多次使用以存储元数据的变体（请参阅下面的唯一标志）。

`meta_value` 可以是字符串、整数或数组。如果是数组，它将在存储到数据库之前自动序列化。

`unique` 标志允许您声明此键是否应唯一。**非**唯一键是文章可以拥有多个变体的键，例如价格。

如果您只希望文章有**一个**价格，则应将其标记为 `unique`，这样 `meta_key` 将只有一个值。

## 更新元数据

如果某个键已存在且您想要更新它，请使用 [`update_post_meta()`](https://developer.wordpress.org/reference/functions/update_post_meta/)。如果您使用此函数而该键**不**存在，那么它将创建该键，就像您使用了 [`add_post_meta()`](https://developer.wordpress.org/reference/functions/add_post_meta/) 一样。

与 [`add_post_meta()`](https://developer.wordpress.org/reference/functions/add_post_meta/) 类似，该函数接受 `post_id`、`meta_key` 和 `meta_value`。它还接受一个可选的 `prev_value` 参数——如果指定了该参数，将导致函数仅更新具有此值的现有元数据条目。如果未提供此参数，函数默认更新所有条目。

## 删除元数据

[`delete_post_meta()`](https://developer.wordpress.org/reference/functions/delete_post_meta/) 函数接收 `post_id`、`meta_key` 参数，以及可选的 `meta_value` 参数。其功能正如其名所示。

## 字符转义

文章元数据值在存储时会经过 [`stripslashes()`](https://www.php.net/manual/en/function.stripslashes.php) 函数处理，因此在传入可能包含转义字符的值（例如 JSON）时需要特别注意。

以 JSON 值 `{"key":"value with \"escaped quotes\""}` 为例：

```
$escaped_json = '{"key":"value with \"escaped quotes\""}';
update_post_meta( $id, 'escaped_json', $escaped_json );
$broken = get_post_meta( $id, 'escaped_json', true );
/*
$broken 经过 stripslashes() 处理后变得无法解析：
{"key":"value with "escaped quotes""}
*/
```

### 解决方法

通过使用函数 [`wp_slash()`](https://developer.wordpress.org/reference/functions/wp_slash/)（在 WP 3.6 中引入）添加额外一层转义，可以抵消对 [`stripslashes()`](https://www.php.net/manual/en/function.stripslashes.php) 的调用：

```
$escaped_json = '{"key":"value with \"escaped quotes\""}';
update_post_meta( $id, 'double_escaped_json', wp_slash( $escaped_json ) );
$fixed = get_post_meta( $id, 'double_escaped_json', true );
/*
$fixed 经过 stripslashes() 处理后，最终得到期望的结果：
{"key":"value with \"escaped quotes\""}
*/
```

## 隐藏自定义字段

如果您是插件或主题开发者，并计划使用自定义字段存储参数，请注意：WordPress 不会在文章编辑界面的自定义字段列表中显示 `meta_key` 以下划线 "_" 开头的字段，使用 [`get_post_meta()`](https://developer.wordpress.org/reference/functions/get_post_meta/) 模板函数时也不会显示。

这有助于通过 [`add_meta_box()`](https://developer.wordpress.org/reference/functions/add_meta_box/) 函数以特殊方式展示这些自定义字段。

以下示例将添加一个独特的自定义字段，其 `meta_key` 为 '_color'，`meta_value` 为 'red'，但该字段不会显示在文章编辑界面：

```
add_post_meta( 68, '_color', 'red', true );
```

### 隐藏数组

此外，如果 `meta_value` 是一个数组，即使你没有在 `meta_key` 名称前添加下划线，它也不会在页面编辑屏幕上显示。

# 渲染文章元数据

以下是不完整的用于获取和渲染文章元数据的函数和[模板标签](https://developer.wordpress.org/themes/basics/template-tags/)列表：

- [`get_post_meta()`](https://developer.wordpress.org/reference/functions/get_post_meta/) – 自动列出文章所有自定义字段的模板标签。
- [`get_post_custom()`](https://developer.wordpress.org/reference/functions/get_post_custom/) 和 [`get_post_meta()`](https://developer.wordpress.org/reference/functions/get_post_meta/) – 检索文章的一个或所有元数据。
- [`get_post_custom_values()`](https://developer.wordpress.org/reference/functions/get_post_custom_values/) – 检索自定义文章字段的值。

# 插件基础

## 快速入门

最简单的 WordPress 插件就是一个带有 WordPress 插件头部注释的 PHP 文件。强烈建议您创建一个目录来存放插件，以便将所有插件文件整齐地组织在一个位置。

请按照以下步骤开始创建新插件。

1. 进入 WordPress 安装目录的 `wp-content/` 文件夹。
2. 打开 `plugins/` 目录。
3. 创建一个新目录，并以插件名称命名（例如 `plugin-name/`）。
4. 打开新建的插件目录。
5. 创建一个新的 PHP 文件（建议以插件名称命名此文件，例如 `plugin-name.php`）。

以下是在 Unix 命令行中执行此过程的示例：

```bash
$ cd wp-content/
$ cd plugins/
$ mkdir plugin-name/
$ cd plugin-name/
$ vim plugin-name.php
```

在上面的示例中，`vim` 是文本编辑器的名称。您可以使用任何您习惯的编辑器。

现在您正在编辑新插件的 PHP 文件，需要添加一个插件头部注释。这是一个特殊格式的 PHP 块注释，包含插件的元数据，例如名称、作者、版本、许可证等。插件头部注释必须符合[头部要求](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/)，并且至少包含插件的名称。

插件文件夹中只能有一个文件包含头部注释——如果插件有多个 PHP 文件，则只有一个文件应包含头部注释。

保存文件后，您应该能在 WordPress 站点中看到您的插件。登录您的 WordPress 站点，在 WordPress 管理后台的左侧导航栏中点击 **插件**。此页面会显示 WordPress 站点拥有的所有插件列表。您的新插件现在应该出现在该列表中！

## 钩子：动作与过滤器

WordPress 钩子允许你在特定节点介入 WordPress，无需修改任何核心文件即可改变其运行方式。

WordPress 包含两种钩子类型：*动作*与*过滤器*。动作允许你添加或修改 WordPress 功能，而过滤器则让你能在内容加载和展示给网站用户时对其进行调整。

钩子并非仅为插件开发者设计；WordPress 核心自身就广泛使用钩子来提供默认功能。其余钩子则是预留的接入点，当你需要调整 WordPress 工作方式时可直接使用。这正是 WordPress 如此灵活的关键所在。

### 基础钩子

创建插件时所需的 3 个基础钩子是 [register_activation_hook()](https://developer.wordpress.org/reference/functions/register_activation_hook/)、[register_deactivation_hook()](https://developer.wordpress.org/reference/functions/register_deactivation_hook/) 和 [register_uninstall_hook()](https://developer.wordpress.org/reference/functions/register_uninstall_hook/)。

[激活钩子](https://developer.wordpress.org/plugins/plugin-basics/activation-deactivation-hooks/)在您*激活*插件时运行。您可以用它来提供设置插件的函数——例如，在 `options` 表中创建一些默认设置。

[停用钩子](https://developer.wordpress.org/plugins/plugin-basics/activation-deactivation-hooks/)在您*停用*插件时运行。您可以用它来提供清除插件存储的任何临时数据的函数。

这些[卸载方法](https://developer.wordpress.org/plugins/plugin-basics/uninstall-methods/)用于在通过 WordPress 后台*删除*插件后进行清理。您可以用它来删除插件创建的所有数据，例如添加到 `options` 表中的任何选项。

### 添加钩子

您可以使用 [do_action()](https://developer.wordpress.org/reference/functions/do_action/) 添加自定义钩子，这将使开发者能够通过您的钩子传递函数来扩展插件。

### 移除钩子

你也可以使用 [remove_action()](https://developer.wordpress.org/reference/functions/remove_action/) 来移除之前定义的函数。例如，如果你的插件是另一个插件的附加组件，你可以使用 [remove_action()](https://developer.wordpress.org/reference/functions/remove_action/) 并指定与前一个插件通过 [add_action()](https://developer.wordpress.org/reference/functions/add_action/) 添加的相同回调函数。在这些情况下，动作的优先级很重要，因为 [remove_action()](https://developer.wordpress.org/reference/functions/remove_action/) 需要在初始的 [add_action()](https://developer.wordpress.org/reference/functions/add_action/) 之后运行。

移除钩子上的动作或更改优先级时应格外小心，因为这些改动可能难以预见会如何影响同一钩子的其他交互。我们强烈建议频繁进行测试。

你可以在本手册的[钩子](https://developer.wordpress.org/plugins/hooks/)章节了解更多关于创建钩子及与其交互的信息。

## WordPress API

您知道 WordPress 提供了多种[应用程序编程接口（API）](https://make.wordpress.org/core/handbook/best-practices/core-apis/)吗？这些 API 可以极大地简化您在插件中需要编写的代码。您肯定不想重复造轮子，尤其是在许多人已经为您完成了大量工作和测试的情况下。

最常用的是[选项 API](https://codex.wordpress.org/Options_API)，它能让您轻松地将插件数据存储到数据库中。如果您考虑在插件中使用 [cURL](https://en.wikipedia.org/wiki/CURL)，那么[HTTP API](https://developer.wordpress.org/plugins/http-api/) 可能会引起您的兴趣。

既然我们讨论的是插件，您应该研究一下[插件 API](https://codex.wordpress.org/Plugin_API)。它提供了多种函数，将协助您开发插件。

## WordPress 如何加载插件

当 WordPress 在管理后台的插件页面加载已安装插件列表时，它会搜索 `plugins/` 文件夹（及其子文件夹）以查找包含 WordPress 插件头部注释的 PHP 文件。如果你的整个插件仅由一个 PHP 文件组成，例如 [Hello Dolly](https://wordpress.org/plugins/hello-dolly/ "Hello Dolly")，该文件可直接位于 `plugins/` 文件夹的根目录中。但更常见的情况是，插件文件会存放在以插件命名的专属文件夹内。

## 分享你的插件

有时你创建的插件仅用于自己的网站。但许多人喜欢与 WordPress 社区分享他们的插件。在分享插件之前，你需要做的一件事是[选择许可证](https://choosealicense.com/)。这能让插件的使用者了解他们被允许如何使用你的代码。为保持与 WordPress 核心的兼容性，建议选择与 GNU 通用公共许可证（GPLv2+）兼容的许可证。

# 激活/停用钩子

激活和停用钩子提供了在插件激活或停用时执行操作的方法。

- 在**激活**时，插件可以运行例程来添加重写规则、添加自定义数据库表或设置默认选项值。
- 在**停用**时，插件可以运行例程来删除临时数据，例如缓存和临时文件及目录。

[alert]停用钩子有时会与[卸载钩子](https://developer.wordpress.org/plugins/plugin-basics/uninstall-methods/)混淆。卸载钩子最适合**永久删除所有数据**，例如删除插件选项和自定义表等。[/alert]

## 激活

要设置激活钩子，请使用 [register\_activation\_hook()](https://developer.wordpress.org/reference/functions/register_activation_hook/) 函数：

```
register_activation_hook(
  __FILE__,
  'pluginprefix_function_to_run'
);
```

## 停用

要设置停用钩子，请使用 [register_deactivation_hook()](https://developer.wordpress.org/reference/functions/register_deactivation_hook/) 函数：

```
register_deactivation_hook(
  __FILE__,
  'pluginprefix_function_to_run'
);
```

这些函数的第一个参数均指向你的主插件文件，即你放置了[插件头部注释](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/)的文件。通常这两个函数会在主插件文件中触发；但如果函数被放置在其他文件中，你必须更新第一个参数以正确指向主插件文件。

## Example

One of the most common uses for an activation hook is to refresh WordPress permalinks when a plugin registers a custom post type. This gets rid of the nasty 404 errors.

Let's look at an example of how to do this:

```
/**
 * Register the "book" custom post type
 */
function pluginprefix_setup_post_type() {
  register_post_type( 'book', ['public' => true ] ); 
} 
add_action( 'init', 'pluginprefix_setup_post_type' );

/**
 * Activate the plugin.
 */
function pluginprefix_activate() { 
  // Trigger our function that registers the custom post type plugin.
  pluginprefix_setup_post_type(); 
  // Clear the permalinks after the post type has been registered.
  flush_rewrite_rules(); 
}
register_activation_hook( __FILE__, 'pluginprefix_activate' );
```

If you are unfamiliar with registering custom post types, don't worry – this will be covered later. This example is used simply because it's very common.

Using the example from above, the following is how to reverse this process and deactivate a plugin:

```
/**
 * Deactivation hook.
 */
function pluginprefix_deactivate() {
  // Unregister the post type, so the rules are no longer in memory.
  unregister_post_type( 'book' );
  // Clear the permalinks to remove our post type's rules from the database.
  flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'pluginprefix_deactivate' );
```

For further information regarding activation and deactivation hooks, here are some excellent resources:

- [register\_activation\_hook()](https://developer.wordpress.org/reference/functions/register_activation_hook/) in the WordPress function reference.
- [register\_deactivation\_hook()](https://developer.wordpress.org/reference/functions/register_deactivation_hook/) in the WordPress function reference.

# 最佳实践

以下是一些最佳实践，可帮助您组织代码，使其与 WordPress 核心和其他 WordPress 插件良好协作。

## 避免命名冲突

当您的插件为变量、函数或类使用了与其他插件相同的名称时，就会发生命名冲突。

幸运的是，您可以通过使用以下方法来避免命名冲突。

## 过程式编码方法

默认情况下，所有变量、函数和类都定义在**全局命名空间**中，这意味着您的插件可能会覆盖其他插件设置的变量、函数和类，反之亦然。在函数或类*内部*定义的变量不受此影响。

### 为所有内容添加前缀

所有全局可访问的代码都应使用一个*唯一*标识符作为前缀。前缀可以防止与其他插件发生冲突，避免它们覆盖你的变量或意外调用你的函数和类。

为了防止与其他插件冲突，你的前缀应至少为 4 个字母，但我们建议使用 5 个。你应该避免使用常见的英文单词，而是选择对你的插件来说独特的东西。仅在 WordPress.org 上我们就托管了数万个插件，在我们的服务器之外还有数十万个。你*必定*会遇到冲突。

一个好的方法是使用前缀。例如，如果你的插件名为 "Easy Custom Post Types"，那么你可以使用以下名称：

- `function ecpt_save_post()`
- `define( 'ECPT_LICENSE', true );`
- `class ECPT_Admin{}`
- `namespace EasyCustomPostTypes;`
- `update_option( 'ecpt_settings', $settings );`

[info]由于你是作为 **WordPress** 项目的一部分编写代码，因此必须避免使用极有可能与 WordPress 核心发生冲突的前缀。这包括但不限于：`__`（双下划线）、`wp_`、`WordPress` 或 `_`（单下划线）。

如果你正在为“子”插件（例如 WooCommerce 扩展）编写代码，同样需要避免使用它们通常/常见的前缀（例如 Woo、WooCommerce）。

你可以在类或命名空间*内部*使用它们，但不能作为独立的函数/命名空间/类。[/info]

如果你使用 `_n()` 或 `__()` 进行翻译，那没问题。我们**仅**讨论你为插件创建的函数，而不是 WordPress 的核心函数。事实上，这些核心功能正是你*不应该*在自己的插件中使用这些前缀的原因！你肯定不希望破坏用户的 WordPress。

请记住：好的前缀名称对你的插件来说是唯一且独特的。这将有助于你和下一个人在调试时，同时防止冲突。

**必须**添加前缀的代码包括：

- 函数（除非已命名空间化）
- 类、接口和特征（除非已命名空间化）
- 命名空间
- 全局变量
- 选项和瞬态数据

### 检查现有实现

PHP 提供了多种函数来验证变量、函数、类和常量是否存在。如果实体存在，所有这些函数都将返回 true。

- **变量**: [isset()](https://www.php.net/manual/en/function.isset.php)（包括数组、对象等）
- **函数**: [function_exists()](https://www.php.net/manual/en/function.function-exists.php)
- **类**: [class_exists()](https://www.php.net/manual/en/function.class-exists.php)
- **常量**: [defined()](https://www.php.net/manual/en/function.defined.php)

请注意，在所有函数和类周围使用 `(!function_exists('NAME')) {` 听起来是个好主意，直到你意识到其致命缺陷。如果其他代码中存在同名函数且其代码先加载，你的插件将会崩溃。使用 if-exists 来替换/覆盖函数或类应仅用于**共享**库。

### Example

```
// Create a function called "wporg_init" if it doesn't already exist
if ( ! function_exists( 'wporg_init' ) ) {
  function wporg_init() {
    register_setting( 'wporg_settings', 'wporg_option_foo' );
  }
}

// Create a function called "wporg_get_foo" if it doesn't already exist
if ( ! function_exists( 'wporg_get_foo' ) ) {
  function wporg_get_foo() {
    return get_option( 'wporg_option_foo' );
  }
}
```

## 面向对象编程方法

解决命名冲突问题的一种更简便方法是使用[类](https://www.php.net/manual/en/language.oop5.php)来组织插件代码。

您仍需注意检查所需类名是否已被占用，其余部分将由 PHP 自动处理。

### Example

```
if ( ! class_exists( 'WPOrg_Plugin' ) ) {
  class WPOrg_Plugin {
    public static function init() {
      register_setting( 'wporg_settings', 'wporg_option_foo' );
    }

    public static function get_foo() {
      return get_option( 'wporg_option_foo' );
    }
  }

  WPOrg_Plugin::init();
  WPOrg_Plugin::get_foo();
}
```

## 文件组织

插件目录的根层级应包含您的 `plugin-name.php` 文件，并可选择性包含 [uninstall.php](https://developer.wordpress.org/plugins/plugin-basics/uninstall-methods/) 文件。所有其他文件应尽可能组织到子文件夹中。

### 文件夹结构

清晰的文件夹结构有助于您和其他参与插件开发的人员将相关文件归类存放。

以下是一个可供参考的示例文件夹结构：

```
/plugin-name
  plugin-name.php
  uninstall.php
  /languages
  /includes
  /admin
    /js
    /css
    /images
  /public
    /js
    /css
    /images
```

## 插件架构

您为插件选择的架构或代码组织方式，很可能取决于插件的大小。

对于与 WordPress 核心、主题或其他插件交互有限的小型单一用途插件，设计复杂的类结构益处不大；除非您知道该插件后续会大幅扩展。

对于包含大量代码的大型插件，请从一开始就考虑使用类。分离样式文件、脚本文件，甚至构建相关文件。这将有助于代码组织和插件的长期维护。

### Conditional Loading

It's helpful to separate your admin code from the public code. Use the conditional [is_admin()](https://developer.wordpress.org/reference/functions/is_admin/). You must still perform capability checks as this doesn't indicate the user is authenticated or has Administrator-level access. See [Checking User Capabilities](https://developer.wordpress.org/plugins/security/checking-user-capabilities/).

For example:

```
if ( is_admin() ) {
  // we are in admin mode
  require_once __DIR__ . '/admin/plugin-name-admin.php';
}
```

### Avoiding Direct File Access

As a security precaution, it's a good practice to disallow access if the `ABSPATH` global is not defined. This is only applicable to files which contain code outside of class or function definitions, such as the main plugin file.

You can implement this by including this code at the top of the file:

```
if ( ! defined( 'ABSPATH' ) ) {
  exit; // Exit if accessed directly
}
```

### 架构模式

虽然存在多种可能的架构模式，但大致可分为三种变体：

- [单一插件文件，包含函数](https://github.com/GaryJones/move-floating-social-bar-in-genesis/blob/master/move-floating-social-bar-in-genesis.php)
- [单一插件文件，包含类、实例化对象及可选函数](https://github.com/norcross/wp-comment-notes/blob/master/wp-comment-notes.php)
- [主插件文件，附带一个或多个类文件](https://github.com/DevinVinson/WordPress-Plugin-Boilerplate)

### 架构模式详解

上述较为复杂的代码组织形式已有具体实现方案，并整理为教程和演示文稿：

- [Slash – 单例模式、加载器、动作、屏幕、处理器](https://jjj.blog/2012/12/slash-architecture-my-approach-to-building-wordpress-plugins/)
- [在 WordPress 插件中实现 MVC 模式](https://iandunn.name/content/presentations/wp-oop-mvc/mvc.php)

## 样板起点

与其为每个新插件从头开始编写，您可能希望从一个**样板**开始。使用样板的一个优势是能保持您自己插件之间的一致性。如果您使用其他人已经熟悉的样板，也更容易让他们为您的代码做出贡献。

这些样板也作为不同但可比较架构的进一步示例。

- [WordPress 插件样板](https://github.com/DevinVinson/WordPress-Plugin-Boilerplate)：一个 WordPress 插件开发的基础，旨在为构建您的插件提供清晰一致的指南。
- [WordPress 插件引导程序](https://github.com/claudiosanches/wordpress-plugin-boilerplate)：使用 Grunt、Compass、GIT 和 SVN 开发 WordPress 插件的基本引导程序。
- [WP 骨架插件](https://github.com/ptahdunbar/wp-skeleton-plugin)：专注于单元测试和使用 composer 进行开发的骨架插件。
- [WP CLI 脚手架](https://developer.wordpress.org/cli/commands/scaffold/plugin/)：WP CLI 的 Scaffold 命令创建一个带有 CI 配置文件等选项的骨架插件。

当然，您可以结合这些以及其他样板的不同方面，创建您自己的自定义样板。

# 确定插件和内容目录

在编写 WordPress 插件时，您经常需要引用 WordPress 安装目录以及您的插件或主题中的各种文件和文件夹。

WordPress 提供了多个函数来轻松确定特定文件或目录的位置。在您的插件中应始终使用这些函数，而不是硬编码对 wp-content 目录的引用或使用 WordPress 内部常量。

[tip]WordPress 允许用户将 wp-content 目录放置在任意位置并随意重命名。切勿假设插件一定位于 wp-content/plugins、上传文件一定位于 wp-content/uploads，或主题一定位于 wp-content/themes。[/tip]

PHP 的 `__FILE__` 魔术常量会自动解析符号链接，因此如果 `wp-content/`、`wp-content/plugins/` 甚至单个插件目录被符号链接，硬编码的路径将无法正常工作。

## 常用方法

如果您的插件包含 JavaScript 文件、CSS 文件或其他外部文件，您可能需要这些文件的 URL 以便将其加载到页面中。为此，您应该使用 [plugins_url()](https://developer.wordpress.org/reference/functions/plugins_url/) 函数，如下所示：

```
plugins_url( 'myscript.js', __FILE__ );
```

这将返回 myscript.js 的完整 URL，例如 `example.com/wp-content/plugins/myplugin/myscript.js`。

要将插件的 JavaScript 或 CSS 加载到页面中，您应分别使用 [`wp_enqueue_script()`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) 或 [`wp_enqueue_style()`](https://developer.wordpress.org/reference/functions/wp_enqueue_style/)，并将 `plugins_url()` 的结果作为文件 URL 传入。

## 可用函数

WordPress 包含许多其他函数，用于确定插件、主题和 WordPress 自身内部文件或目录的路径和 URL。有关其用法的完整信息，请参阅每个函数的独立 DevHub 页面。

### Plugins

- [`plugins_url()`](https://developer.wordpress.org/reference/functions/plugins_url/)
- [`plugin_dir_url()`](https://developer.wordpress.org/reference/functions/plugin_dir_url/)
- [`plugin_dir_path()`](https://developer.wordpress.org/reference/functions/plugin_dir_path/)
- [`plugin_basename()`](https://developer.wordpress.org/reference/functions/plugin_basename/)

### Themes

- [`get_template_directory_uri()`](https://developer.wordpress.org/reference/functions/get_template_directory_uri/)
- [`get_stylesheet_directory_uri()`](https://developer.wordpress.org/reference/functions/get_stylesheet_directory_uri/)
- [`get_stylesheet_uri()`](https://developer.wordpress.org/reference/functions/get_stylesheet_uri/)
- [`get_theme_root_uri()`](https://developer.wordpress.org/reference/functions/get_theme_root_uri/)
- [`get_theme_root()`](https://developer.wordpress.org/reference/functions/get_theme_root/)
- [`get_theme_roots()`](https://developer.wordpress.org/reference/functions/get_theme_roots/)
- [`get_stylesheet_directory()`](https://developer.wordpress.org/reference/functions/get_stylesheet_directory/)
- [`get_template_directory()`](https://developer.wordpress.org/reference/functions/get_template_directory/)

### Site Home

- [`home_url()`](https://developer.wordpress.org/reference/functions/home_url/)
- [`get_home_path()`](https://developer.wordpress.org/reference/functions/get_home_path/)

### WordPress

- [`admin_url()`](https://developer.wordpress.org/reference/functions/admin_url/)
- [`site_url()`](https://developer.wordpress.org/reference/functions/site_url/)
- [`content_url()`](https://developer.wordpress.org/reference/functions/content_url/)
- [`includes_url()`](https://developer.wordpress.org/reference/functions/includes_url/)
- [`wp_upload_dir()`](https://developer.wordpress.org/reference/functions/wp_upload_dir/)

### Multisite

- [`get_admin_url()`](https://developer.wordpress.org/reference/functions/get_admin_url/)
- [`get_home_url()`](https://developer.wordpress.org/reference/functions/get_home_url/)
- [`get_site_url()`](https://developer.wordpress.org/reference/functions/get_site_url/)
- [`network_admin_url()`](https://developer.wordpress.org/reference/functions/network_admin_url/)
- [`network_site_url()`](https://developer.wordpress.org/reference/functions/network_site_url/)
- [`network_home_url()`](https://developer.wordpress.org/reference/functions/network_home_url/)

## 常量

WordPress 在确定内容和插件目录路径时会使用以下常量。插件或主题不应直接使用这些常量，但为保持完整性，此处列出它们。

```
WP_CONTENT_DIR  // 无尾部斜杠，仅完整路径
WP_CONTENT_URL  // 完整 URL
WP_PLUGIN_DIR  // 完整路径，无尾部斜杠
WP_PLUGIN_URL  // 完整 URL，无尾部斜杠

// 多站点默认可用，单站点安装中未设置
// 可在单站点安装中使用（通常：风险自负）
UPLOADS // （若设置，上传文件夹，相对于 ABSPATH）（例如：/wp-content/uploads）
```

## Related

### WordPress Directories

| Function | | Directory |
| --- | --- | --- |
| [`home_url()`](https://developer.wordpress.org/reference/functions/home_url/) | Home URL | `https://www.example.com` |
| [`site_url()`](https://developer.wordpress.org/reference/functions/site_url/) | Site directory URL | `https://www.example.com` or `https://www.example.com/wordpress` |
| [`admin_url()`](https://developer.wordpress.org/reference/functions/admin_url/) | Admin directory URL | `https://www.example.com/wp-admin` |
| [`includes_url()`](https://developer.wordpress.org/reference/functions/includes_url/) | Includes directory URL | `https://www.example.com/wp-includes` |
| [`content_url()`](https://developer.wordpress.org/reference/functions/content_url/) | Content directory URL | `https://www.example.com/wp-content` |
| [`plugins_url()`](https://developer.wordpress.org/reference/functions/plugins_url/) | Plugins directory URL | `https://www.example.com/wp-content/plugins` |
| [`wp_upload_dir()`](https://developer.wordpress.org/reference/functions/wp_upload_dir/) | Upload directory URL (returns an array) | `https://www.example.com/wp-content/uploads` |

# 头部要求

如[入门指南](https://developer.wordpress.org/plugins/plugin-basics/#getting-started)所述，主 PHP 文件应包含头部注释，以告知 WordPress 该文件是一个插件并提供插件相关信息。

## 最低要求字段

至少，头部注释必须包含插件名称：

```
/*
 * Plugin Name: 你的插件名称
 */
```

## 头部字段

可用的头部字段：

- **Plugin Name:** (_必需_) 你的插件名称，将显示在 WordPress 管理后台的插件列表中。
- **Plugin URI:** 插件的主页，应是一个唯一的 URL，最好在你自己的网站上。这 _必须是你的插件独有的_。你不能在此处使用 WordPress.org 的 URL。
- **Description:** 插件的简短描述，显示在 WordPress 管理后台的插件部分。请将此描述控制在 140 个字符以内。
- **Version:** 插件的当前版本号，例如 1.0 或 1.0.3。
- **Requires at least:** 插件能正常运行所需的最低 WordPress 版本。
- **Requires PHP:** 所需的最低 PHP 版本。
- **Author:** 插件作者的名字。多个作者可以使用逗号分隔列出。
- **Author URI:** 作者的网站或在其他网站（如 WordPress.org）上的个人资料页。
- **License:** 插件许可证的简称（slug），例如 GPLv2。更多关于许可证的信息可以在 [WordPress.org 指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#1-plugins-must-be-compatible-with-the-gnu-general-public-license) 中找到。
- **License URI:** 指向许可证全文的链接（例如 [https://www.gnu.org/licenses/old-licenses/gpl-2.0.html](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)）。
- **Text Domain:** 插件的 [gettext](https://www.gnu.org/software/gettext/) 文本域。更多信息可以在 [如何国际化你的插件](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/) 页面的 [文本域](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#text-domains) 部分找到。
- **Domain Path:** 域路径让 WordPress 知道在哪里可以找到翻译文件。更多信息可以在 [如何国际化你的插件](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/) 页面的 [域路径](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#domain-path) 部分找到。
- **Network:** 插件是否只能在整个网络范围内激活。只能设置为 _true_，不需要时应省略。
- **Update URI:** **(_重要：切勿用于托管在 WordPress.org 插件目录中的插件_)** 允许非 WordPress.org 插件避免被 WordPress.org 插件目录中同名插件的更新意外覆盖。更多信息请阅读相关的 [开发说明](https://make.wordpress.org/core/2021/06/29/introducing-update-uri-plugin-header-in-wordpress-5-8/)。

一个包含头部注释的有效 PHP 文件可能如下所示：

```
/*
 * Plugin Name:       My Basics Plugin
 * Plugin URI:        https://example.com/plugins/the-basics/
 * Description:       Handle the basics with this plugin.
 * Version:           1.10.3
 * Requires at least: 5.2
 * Requires PHP:      7.2
 * Author:            John Smith
 * Author URI:        https://author.example.com/
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       my-basics-plugin
 * Domain Path:       /languages
 */
```

Here's another example which allows file-level PHPDoc DocBlock as well as WordPress plugin file headers:

```
/**
 * Plugin Name
 *
 * @package           PluginPackage
 * @author            Your Name
 * @copyright         2019 Your Name or Company Name
 * @license           GPL-2.0-or-later
 *
 * @wordpress-plugin
 * Plugin Name:       Plugin Name
 * Plugin URI:        https://example.com/plugin-name
 * Description:       Description of the plugin.
 * Version:           1.0.0
 * Requires at least: 5.2
 * Requires PHP:      7.2
 * Author:            Your Name
 * Author URI:        https://example.com
 * Text Domain:       plugin-slug
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.txt
 */
```

## 注意事项

[alert]为项目分配版本号时，请注意 WordPress 使用 PHP `version_compare()` 函数来比较插件版本号。因此，在发布插件新版本前，应确保此 PHP 函数认为新版本"大于"旧版本。例如，1.02 实际上大于 1.1。[/alert]

# Including a Software License

Most WordPress plugins are released under the [GPL](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html), which is the same license that [WordPress itself uses](https://wordpress.org/about/license/). However, there are other compatible options available. It is always best to clearly indicate the license your plugin uses.

In the [Header Requirements](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) section, we briefly mentioned how you can indicate your plugin's license within the plugin header comment. Another common, and encouraged, practice is to place a license block comment near the top of your main plugin file (the same one that has the plugin header comment).

This license block comment usually looks something like this:

```
/*
{Plugin Name} is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
any later version.

{Plugin Name} is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with {Plugin Name}. If not, see {URI to Plugin License}.
*/
```

# 卸载方法

当您的插件从网站卸载时，可能需要执行一些清理操作。

如果用户已停用插件，然后在 WordPress 管理后台点击删除链接，该插件即被视为已卸载。

当您的插件被卸载时，您需要清除所有插件特定的选项和/或设置，以及/或其他数据库实体（如表）。

经验不足的开发人员有时会错误地为此目的使用停用钩子。

下表说明了停用与卸载之间的区别。

| 场景 | 停用钩子 | 卸载钩子 |
| --- | --- | --- |
| 清除缓存/临时文件 | 是 | 否 |
| 刷新固定链接 | 是 | 否 |
| 从 {[$wpdb](https://developer.wordpress.org/reference/classes/wpdb/)→prefix}_options 表中移除选项 | 否 | 是 |
| 从 [$wpdb](https://developer.wordpress.org/reference/classes/wpdb/) 中移除表 | 否 | 是 |

## 方法一：register_uninstall_hook

要设置卸载钩子，请使用 [register\_uninstall\_hook()](https://developer.wordpress.org/reference/functions/register_uninstall_hook/) 函数：

```
register_uninstall_hook(
  __FILE__,
  'pluginprefix_function_to_run'
);
```

## 方法二：uninstall.php

要使用此方法，您需要在插件的根文件夹中创建一个 `uninstall.php` 文件。当用户删除插件时，此特殊文件会自动运行。

例如：`/plugin-name/uninstall.php`

[alert]在 `uninstall.php` 中执行任何操作之前，请务必检查常量 `WP_UNINSTALL_PLUGIN` 是否存在。这可以防止直接访问。

该常量将在 `uninstall.php` 调用期间由 WordPress 定义。

当通过 [register\_uninstall\_hook()](https://developer.wordpress.org/reference/functions/register_uninstall_hook/) 执行卸载时，该常量**不会**被定义。[/alert]

以下是一个删除选项条目和删除数据库表的示例：

```
// 如果 uninstall.php 不是由 WordPress 调用，则退出
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
  die;
}

$option_name = 'wporg_option';

delete_option( $option_name );

// 针对多站点中的站点选项
delete_site_option( $option_name );

// 删除自定义数据库表
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mytable" );
```

[info]在多站点环境中，遍历所有博客以删除选项可能会非常消耗资源。[/info]

# 自定义文章类型

WordPress 将文章类型存储在 `posts` 表中，允许开发者注册自定义文章类型以及已存在的类型。

本章将展示如何[注册自定义文章类型](https://developer.wordpress.org/plugins/post-types/registering-custom-post-types/)，以及如何[从数据库检索其内容并呈现给公众](https://developer.wordpress.org/plugins/post-types/working-with-custom-post-types/)。

# 注册自定义文章类型

WordPress 默认包含五种文章类型：`post`、`page`、`attachment`、`revision` 和 `menu`。

在开发插件时，您可能需要创建特定的内容类型：例如，电子商务网站的产品、在线学习网站的作业，或影评网站的影片。

通过自定义文章类型，您可以注册自己的文章类型。一旦自定义文章类型注册成功，它将获得一个新的顶级管理界面，用于管理和创建该类型的文章。

要注册新的文章类型，请使用 [`register_post_type()`](https://developer.wordpress.org/reference/functions/register_post_type/) 函数。

[alert]我们建议将自定义文章类型放在插件中，而不是主题中。这样可以确保即使用户更换主题，其内容仍然可以移植。[/alert]

以下是一个最小示例，注册了一个名为“产品”的新文章类型，在数据库中的标识符为 `wporg_product`。

```
function wporg_custom_post_type() {
  register_post_type('wporg_product',
    array(
      'labels'      => array(
        'name'          => __('Products', 'textdomain'),
        'singular_name' => __('Product', 'textdomain'),
      ),
      'public'      => true,
      'has_archive' => true,
    )
  );
}
add_action('init', 'wporg_custom_post_type');
```

请访问 [`register_post_type()`](https://developer.wordpress.org/reference/functions/register_post_type/) 的参考页面查看参数说明。

[warning]您必须在 [`admin_init`](https://developer.wordpress.org/reference/hooks/admin_init/) 钩子之前和 [`after_setup_theme`](https://developer.wordpress.org/reference/hooks/after_setup_theme/) 钩子之后调用 `register_post_type()`。推荐使用 [`init`](https://developer.wordpress.org/reference/hooks/init/) 动作钩子。[/warning]

## 命名最佳实践

请务必为您的文章类型函数和标识符添加与您的插件、主题或网站对应的简短前缀。

[warning]**请确保您的自定义文章类型标识符不超过 20 个字符**，因为数据库中 `post_type` 列目前是该长度的 VARCHAR 字段。[/warning]

[warning]**为确保向前兼容**，请勿使用 `wp_` 作为您的标识符——它已被 WordPress 核心使用。[/warning]

[warning]如果您的标识符过于通用（例如：`product`），可能会与选择使用相同标识符的其他插件或主题发生冲突。[/warning]

[info]**若不禁用其中一个冲突的文章类型，则无法解决重复的文章类型标识符问题。**[/info]

## URL

自定义文章类型在网站 URL 结构中拥有其专属的固定链接段。

类型为 `wporg_product` 的文章默认使用以下 URL 结构：`https://example.com/wporg_product/%product_name%`。

其中 `wporg_product` 是自定义文章类型的固定链接段，`%product_name%` 则是特定产品的固定链接段。

最终的固定链接将呈现为：`https://example.com/wporg_product/wporg-is-awesome`。

您可以在自定义文章类型的编辑界面查看固定链接，操作方式与默认文章类型完全一致。

### A Custom Slug for a Custom Post Type

To set a custom slug for the slug of your custom post type all you need to do is add a key => value pair to the `rewrite` key in the `register_post_type()` arguments array.

Example:

```
function wporg_custom_post_type() {
  register_post_type('wporg_product',
    array(
      'labels'      => array(
        'name'          => __( 'Products', 'textdomain' ),
        'singular_name' => __( 'Product', 'textdomain' ),
      ),
      'public'      => true,
      'has_archive' => true,
      'rewrite'     => array( 'slug' => 'products' ), // my custom slug
    )
  );
}
add_action('init', 'wporg_custom_post_type');
```

The above will result in the following URL structure: `https://example.com/products/%product_name%`

[warning]Using a generic slug like `products` can potentially conflict with other plugins or themes, so try to use one that is more specific to your content.[/warning]

[info]Unlike the custom post type identifiers, the duplicate slug problem can be solved easily by changing the slug for one of the conflicting post types.[/info]

If the plugin author included an `apply_filters()` call on the arguments, this can be done programmatically by overriding the arguments submitted via the `register_post_type()` function.

# Working with Custom Post Types

## Custom Post Type Templates

You can create custom [templates](https://developer.wordpress.org/themes/core-concepts/theme-structure/) for your custom post types. In the same way posts and their archives can be displayed using `single.php` and `archive.php`, you can create the templates:

- `single-{post_type}.php` – for single posts of a custom post type
- `archive-{post_type}.php` – for the archive

Where `{post_type}` is the post type identifier, used as the `$post_type` argument of the `register_post_type()` function.

Building upon what we’ve learned previously, you could create `single-wporg_product.php` and `archive-wporg_product.php` template files for single product posts and the archive.

Alternatively, you can use the [`is_post_type_archive()`](https://developer.wordpress.org/reference/functions/is_post_type_archive/) function in any template file to check if the query shows an archive page of a given post type, and the [`post_type_archive_title()`](https://developer.wordpress.org/reference/functions/post_type_archive_title/)  function to display the post type title.

## Querying by Post Type

You can query posts of a specific type by passing the `post_type` key in the arguments array of the `WP_Query` class constructor.

```
<?php
$args = array(
  'post_type'      => 'product',
  'posts_per_page' => 10,
);
$loop = new WP_Query($args);
while ( $loop->have_posts() ) {
  $loop->the_post();
  ?>
  <div class="entry-content">
    <?php the_title(); ?>
    <?php the_content(); ?>
  </div>
  <?php
}
```

This loops through the latest ten product posts and displays the title and content of them one by one.

## Altering the Main Query

Registering a custom post type does not mean it gets added to the main query automatically.

If you want your custom post type posts to show up on standard archives or include them on your home page mixed up with other post types, use the [`pre_get_posts`](https://developer.wordpress.org/reference/hooks/pre_get_posts/) action hook.

The next example will show posts from `post`, `page` and `movie` post types on the home page:

```
function wporg_add_custom_post_types($query) {
  if ( is_home() && $query->is_main_query() ) {
    $query->set( 'post_type', array( 'post', 'page', 'movie' ) );
  }
  return $query;
}
add_action('pre_get_posts', 'wporg_add_custom_post_types');
```

# 隐私

您是否正在开发处理个人数据的插件——例如姓名、地址及其他可用于识别个人身份的信息？您需要谨慎处理这些数据，保护用户和访客的隐私。

## 什么是隐私？

WordPress.org 在欧洲《通用数据保护条例》实施前进行了多项改进。在此项工作启动后，我们已将隐私确立为核心 trac 开发的长期重点，这将使我们能够在特定法规之外持续加强隐私和数据保护。

但哪些问题属于"隐私"的范畴？我们又如何定义它？尽管不同国家、文化和法律体系对隐私的要求差异很大，但存在一些适用于任何情境的通用原则：

- **同意与选择权：** 让用户（和网站访客）对其数据的使用拥有选择和决定权，并要求明确、具体且知情的主动同意；
- **目的合法性与明确性：** 仅出于既定目的收集和使用个人数据，且该目的已事先明确告知用户；
- **收集限制：** 仅收集必要的用户数据；尽量避免制作数据额外副本或将数据与其他插件的数据合并；
- **数据最小化：** 将数据处理范围及数据访问人员限制在必要的最小使用需求和最少人员范围内；
- **使用、保留与披露限制：** 及时删除不再需要的数据（包括活跃使用和存档数据），接收方及任何第三方均需执行；
- **准确性与质量：** 确保收集和使用的数据正确、相关且最新，尤其是在数据不准确或质量差可能对用户产生不利影响时；
- **公开性、透明度与告知：** 告知用户其数据如何被收集、使用和共享，以及他们对这些使用方式享有的任何权利；
- **个人参与与访问权：** 为用户提供访问或下载其数据的方法；
- **问责制：** 记录数据使用情况，在传输和第三方使用过程中予以保护，并尽可能防止滥用和泄露；
- **信息安全：** 通过适当的技术和安全措施保护数据；
- **隐私合规性：** 确保工作符合其收集和处理个人数据所在地区的隐私法规。

（来源：[ISO 29100/隐私框架标准](https://www.iso.org/standard/45123.html)）

虽然并非所有原则都适用于所有情境和用途，但在开发过程中运用这些原则有助于确保用户信任。

## 隐私保护设计

许多这些原则都体现在隐私保护设计框架中，该框架指出：

- 隐私保护应具有前瞻性而非被动应对，必须在问题触及用户前预见隐私问题。隐私保护也应是预防性的，而非补救性的。
- 隐私应作为默认设置。用户不应被迫采取行动来保护隐私，数据共享的同意也不应被假定。
- 隐私应作为核心功能融入设计，而非附加组件。
- 隐私应是正和博弈：隐私与安全、隐私与保障、隐私与服务提供之间不应存在权衡取舍。
- 隐私应通过数据最小化、最短保留期和定期删除不再需要的数据，提供端到端的全生命周期保护。
- 插件（及服务，如适用）采用的隐私标准应可见、透明、开放、有文档记录且可独立验证。
- 隐私应以用户为中心。应为用户提供诸如细粒度隐私选择、最大化隐私默认设置、详细隐私信息通知、用户友好选项以及变更清晰告知等选择。

## 为你的插件提供思考方向

为了帮助你的插件做好准备，我们建议为你制作的每个插件都思考以下问题列表：

1. 你的插件如何处理个人数据？使用 [`wp_add_privacy_policy_content()`](https://developer.wordpress.org/reference/functions/wp_add_privacy_policy_content/) 向用户披露以下任何情况：
	- 插件是否与第三方共享个人数据（例如外部 API/服务器）。如果是，它共享哪些数据给哪些第三方，他们是否有已发布的隐私政策可供提供链接？
	- 插件是否收集个人数据？如果是，收集什么数据以及存储在哪里？考虑用户数据/元数据、选项、文章元数据、自定义表、文件等位置。
	- 插件是否使用他人收集的个人数据？如果是，是什么数据？插件是否将个人数据传递给 SDK？该 SDK 如何处理这些数据？
	- 插件是否直接或间接收集遥测数据？例如，每次安装时从第三方源加载图像可能会间接记录和跟踪所有插件安装的使用数据。
	- 插件是否从第三方排队加载 JavaScript、跟踪像素或嵌入 iframe（第三方 JS、跟踪像素和 iframe 可能收集访问者数据/操作、留下 cookie 等）？
	- 插件是否在浏览器中存储内容？如果是，存储在哪里以及存储什么？考虑 cookie、本地存储等。
2. 如果你的插件收集个人数据…
	- 它是否提供个人数据导出器？
	- 它是否提供个人数据擦除回调？
	- 插件出于什么原因（如果有）拒绝擦除个人数据？（例如订单尚未完成等）——这些也应披露。
3. 插件是否使用错误日志记录？它是否尽可能避免记录个人数据？你是否可以使用 [`wp_privacy_anonymize_data()`](https://developer.wordpress.org/reference/functions/wp_privacy_anonymize_data/) 等方法来最小化记录的个人数据？日志条目保留多长时间？谁可以访问它们？
4. 在 wp-admin 中，访问/查看个人数据需要什么角色/权限？这些权限是否足够？
5. 插件在网站前端暴露哪些个人数据？它对登录和未登录用户都显示吗？应该这样吗？
6. 插件在 REST API 端点中暴露哪些个人数据？它对登录和未登录用户都显示吗？查看它需要什么角色/权限？这些是否合适？
7. 插件是否正确移除/清理数据，特别是个人数据：
	- 在卸载插件期间？
	- 当相关项目被删除时（例如从文章元数据或另一个表中任何引用文章的行）？
	- 当用户被删除时（例如从表中任何引用用户的行）？
8. 插件是否提供控件以减少所需的个人数据量？
9. 插件是否仅在 SDK 或 API 需要时才与 SDK 或 API 共享个人数据，还是插件也共享可选的个人数据？
10. 当安装某些其他插件时，此插件收集或共享的个人数据量是否会发生变化？

## 外部资源

* [隐私博客](https://privacy.blog/)
* [WordPress.org 隐私政策](https://wordpress.org/about/privacy/)

# 为您的插件添加个人数据擦除功能

在 WordPress 4.9.6 版本中，新增了工具以简化对欧盟《通用数据保护条例》（简称 GDPR）等法规的合规操作。新增工具包括个人数据移除工具，支持擦除/匿名化指定用户的个人数据。该工具**不会**删除已注册的用户账户——这仍是管理员可选择是否执行的独立步骤。

除了存储在 WordPress 评论等内容中的个人数据外，插件还可以通过钩子接入擦除功能，以清除它们收集的个人数据，无论这些数据是存储在 postmeta 中还是全新的自定义文章类型（CPT）中。

与导出器类似，所有擦除器的“密钥”是用户的电子邮件地址——选择此方式是因为它既支持擦除已注册用户的个人数据，也支持擦除未注册用户（例如未登录的评论者）的数据。

然而，由于执行个人数据擦除是破坏性操作，我们不能未经确认就直接执行。因此，面向管理员的用户界面会要求管理员输入提出请求的用户名或电子邮件地址来启动所有请求，然后向该地址发送确认链接。请求确认后，管理员可以启动用户的个人数据擦除，或在需要时强制执行。

个人数据导出的擦除方式与个人数据导出器类似——依赖于挂接“擦除器”回调函数来执行数据擦除的实际工作。当管理员点击移除个人数据链接时，系统会启动一个 AJAX 循环，逐个遍历系统中注册的所有擦除器。除了核心内置的擦除器，插件也可以注册自己的擦除器回调函数。

擦除器回调接口的设计力求简洁。擦除器回调函数接收待处理的电子邮件地址和页码参数。页码参数（从 1 开始）用于避免插件因尝试一次性擦除所有收集的个人数据而导致超时。规范的插件会限制每页尝试擦除的数据量（例如 100 篇文章、200 条评论等）。

擦除器回调函数会返回：是否已擦除包含个人数据的项目、是否保留了任何包含个人数据的项目、向管理员显示的消息数组（解释保留项目的原因）以及操作是否完成。如果擦除器回调报告未完成，系统将以页码参数加 1 的方式再次调用该回调函数（通过独立请求）。

当所有导出器都完成处理后，管理员界面将更新显示是否已擦除所有发现的个人数据，以及解释个人数据保留原因的相关消息。

Let's work on a hypothetical plugin which adds commenter location data to comments. Let's assume the plugin has used [`add_comment_meta`](https://developer.wordpress.org/reference/functions/add_comment_meta/) to add location data using `meta_key`'s of `latitude` and `longitude`.

The first thing the plugin needs to do is to create an eraser function that accepts an email address and a page, e.g.:

```
/**
 * Removes any stored location data from a user's comment meta for the supplied email address.
 *
 * @param string $email_address   email address to manipulate
 * @param int    $page            pagination
 *
 * @return array
 */
function wporg_remove_location_meta_from_comments_for_email( $email_address, $page = 1 ) {
  $number = 500; // Limit us to avoid timing out
  $page   = (int) $page;

  $comments = get_comments(
    array(
      'author_email' => $email_address,
      'number'       => $number,
      'paged'        => $page,
      'order_by'     => 'comment_ID',
      'order'        => 'ASC',
    )
  );

  $items_removed = false;

  foreach ( (array) $comments as $comment ) {
    $latitude  = get_comment_meta( $comment->comment_ID, 'latitude', true );
    $longitude = get_comment_meta( $comment->comment_ID, 'longitude', true );

    if ( ! empty( $latitude ) ) {
      delete_comment_meta( $comment->comment_ID, 'latitude' );
      $items_removed = true;
    }

    if ( ! empty( $longitude ) ) {
      delete_comment_meta( $comment->comment_ID, 'longitude' );
      $items_removed = true;
    }
  }

  // Tell core if we have more comments to work on still
  $done = count( $comments ) < $number;
  return array(
    'items_removed'  => $items_removed,
    'items_retained' => false, // always false in this example
    'messages'       => array(), // no messages in this example
    'done'           => $done,
  );
}
```

The next thing the plugin needs to do is to register the callback by filtering the eraser array using the [`wp_privacy_personal_data_erasers`](https://developer.wordpress.org/reference/hooks/wp_privacy_personal_data_erasers/) filter.

When registering you provide a friendly name for the eraser (to aid in debugging – this friendly name is not shown to anyone at this time) and the callback, e.g.

```
/**
 * Registers all data erasers.
 *
 * @param array $exporters
 *
 * @return mixed
 */
function wporg_register_privacy_erasers( $erasers ) {
  $erasers['my-plugin-slug'] = array(
    'eraser_friendly_name' => __( 'Comment Location Plugin', 'text-domain' ),
    'callback'             => 'wporg_remove_location_meta_from_comments_for_email',
  );
  return $erasers;
}

add_filter( 'wp_privacy_personal_data_erasers', 'wporg_register_privacy_erasers' );
```

And that's all there is to it! Your plugin will now clean up its personal data!

# Index

## 为插件添加个人数据导出功能

在 WordPress 4.9.6 版本中，新增了工具以简化对欧盟《通用数据保护条例》（简称 GDPR）等法律的合规流程。新增工具中包含个人数据导出功能，支持将指定用户的所有个人数据以 ZIP 文件格式导出。除了存储在 WordPress 评论等模块中的个人数据外，插件也可以通过挂接导出器功能来导出其收集的个人数据，无论这些数据是存储在 postmeta 中还是全新的自定义文章类型（CPT）中。

所有导出的"关键标识"是用户的电子邮件地址——选择此标识是因为它既支持导出已注册用户的个人数据，也支持导出未注册用户（例如未登录的评论者）的数据。

但由于整理个人数据导出可能涉及大量处理过程且可能包含敏感信息，我们不会未经确认就直接生成并发送给请求者。因此，面向管理员的操作界面会要求管理员先输入请求者的用户名或邮箱地址，然后向请求者发送确认链接。

确认请求后，管理员可以生成并下载或直接通过邮件发送个人数据导出 ZIP 文件，也可在需要时随时执行导出操作。用户收到的 ZIP 文件中包含一个"微型网站"，其中的 HTML 索引页面将个人数据按组分类呈现（例如评论组等）。

无论管理员选择下载还是直接发送导出文件，个人数据的汇编方式完全一致——都依赖于挂接"导出器"回调函数来执行数据收集工作。当管理员点击下载或邮件链接时，系统会通过 AJAX 循环依次调用所有已注册的导出器。除了核心内置的导出器，插件也可以注册自己的导出器回调函数。

导出器回调接口的设计力求简洁。导出器回调函数会接收待处理的邮箱地址和页码参数。页码参数（从 1 开始）用于避免插件因一次性导出所有收集数据而导致超时。规范的插件会限制每页处理的数据量（例如 100 篇文章、200 条评论等）。

The exporter callback replies with whatever data it has for that email address and page and whether it is done or not. If a exporter callback reports that it is not done, it will be called again (in a separate request) with the page parameter incremented by 1. Exporter callbacks are expected to return an array of items for the export. Each item contains an a group identifier for the group of which the item is a part (e.g. comments, posts, orders, etc.), an optional group label (translated), an item identifier (e.g. comment-133) and then an array of name, value pairs containing the data to be exported for that item.

It is noteworthy that the value could be a media path, in which case a link to the media file will be added to the index HTML page in the export.

When all the exporters have been called to completion, WordPress first assembles an "index" HTML document that serves as the heart of the export report. If a plugin reports additional data for an item that WordPress or another plugin has already added, all the data for that item will be presented together.

Exports are cached on the server for 3 days and then deleted.

A plugin can register one or more exporters, but most plugins will only need one. Let's work on a hypothetical plugin which adds location data for the commenter to comments.

First, let's assume the plugin has used [`add_comment_meta`](https://developer.wordpress.org/reference/functions/add_comment_meta/) to add location data using `meta_key`'s of `latitude` and `longitude`.

The first thing the plugin needs to do is to create an exporter function that accepts an email address and a page, e.g.:

```
/**
 * Export user meta for a user using the supplied email.
 *
 * @param string $email_address   email address to manipulate
 * @param int    $page            pagination
 *
 * @return array
 */
function wporg_export_user_data_by_email( $email_address, $page = 1 ) {
  $number = 500; // Limit us to avoid timing out
  $page   = (int) $page;

  $export_items = array();

  $comments = get_comments(
    array(
      'author_email' => $email_address,
      'number'       => $number,
      'paged'        => $page,
      'order_by'     => 'comment_ID',
      'order'        => 'ASC',
    )
  );

  foreach ( (array) $comments as $comment ) {
    $latitude  = get_comment_meta( $comment->comment_ID, 'latitude', true );
    $longitude = get_comment_meta( $comment->comment_ID, 'longitude', true );

    // Only add location data to the export if it is not empty.
    if ( ! empty( $latitude ) ) {
      // Most item IDs should look like postType-postID. If you don't have a post, comment or other ID to work with,
      // use a unique value to avoid having this item's export combined in the final report with other items
      // of the same id.
      $item_id = "comment-{$comment->comment_ID}";

      // Core group IDs include 'comments', 'posts', etc. But you can add your own group IDs as needed
      $group_id = 'comments';

// 可选的组标签。核心功能为核心组提供这些标签。如果你定义自己的组，第一个
      // 包含标签的导出器将被用作最终导出报告中的组标签。
      $group_label = __( 'Comments', 'text-domain' );

      // 插件可以在项目数据数组中添加任意数量的项目。
      $data = array(
        array(
          'name'  => __( 'Commenter Latitude', 'text-domain' ),
          'value' => $latitude,
        ),
        array(
          'name'  => __( 'Commenter Longitude', 'text-domain' ),
          'value' => $longitude,
        ),
      );

      $export_items[] = array(
        'group_id'    => $group_id,
        'group_label' => $group_label,
        'item_id'     => $item_id,
        'data'        => $data,
      );
    }
  }

  // 告知核心是否还有更多评论需要处理。
  $done = count( $comments ) > $number;
  return array(
    'data' => $export_items,
    'done' => $done,
  );
}
```

插件接下来需要做的是通过使用 [`wp_privacy_personal_data_exporters`](https://developer.wordpress.org/reference/hooks/wp_privacy_personal_data_exporters/) 过滤器过滤导出器数组来注册回调函数。

注册时，你需要为导出提供一个友好的名称（用于辅助调试——目前此友好名称不会向任何人显示）以及回调函数，例如：

```
/**
 * 注册所有数据导出器。
 *
 * @param array $exporters
 *
 * @return mixed
 */
function wporg_register_user_data_exporters( $exporters ) {
  $exporters['my-plugin-slug'] = array(
    'exporter_friendly_name' => __( 'Comment Location Plugin', 'text-domain' ),
    'callback'               => 'my_plugin_exporter',
  );
  return $exporters;
}

add_filter( 'wp_privacy_personal_data_exporters', 'wporg_register_user_data_exporters' );
```

这样就完成了！你的插件现在将为导出提供数据！

# 隐私相关选项、钩子和能力

隐私工具最初在 WordPress 4.9.6 版本中引入。这些工具旨在允许（并鼓励）开发者将其用作隐私导出器、隐私擦除器和隐私政策指南的一部分。

此后，引入了若干新钩子以扩展可用功能。这些钩子允许开发者在导出和擦除请求中包含额外的个人数据，并为隐私政策指南提供建议内容。

除了控制这些工具的能力外，还新增了多个用于请求和确认邮件的过滤器，从而实现对通知的更精细控制。

## 选项

- `wp_page_for_privacy_policy` – 包含网站隐私政策页面的页面 ID。

## 操作

- `user_request_action_confirmed` – 当用户确认隐私请求时触发。
- `wp_privacy_delete_old_export_files` – 一个计划任务，用于从个人数据导出文件夹中清理旧的导出文件。
- `wp_privacy_personal_data_erased` – 在最后一个擦除器的最后一页完成后触发。
- `wp_privacy_personal_data_export_file` – 用于在导出流程中创建个人数据导出文件。
- `wp_privacy_personal_data_export_file_created` – 在个人数据导出文件创建后触发。

## 过滤器

- `privacy_policy_url` – 过滤隐私政策页面的 URL。
- `the_privacy_policy_link` – 过滤隐私政策页面链接的 HTML。
- `wp_get_default_privacy_policy_content` – 过滤通过隐私政策指南建议包含的默认内容。
- `user_request_action_confirmed_message` – 允许修改向用户显示的操作确认消息。
- `user_request_action_description` – 过滤用户操作描述。
- `user_request_action_email_content` – 过滤尝试账户操作时发送的电子邮件正文。
- `user_request_action_email_headers` – 过滤尝试账户操作时发送的电子邮件标头。
- `user_request_action_email_subject` – 过滤尝试账户操作时发送的电子邮件主题。
- `user_request_confirmed_email_content` – 过滤用户请求确认电子邮件的正文。
- `user_request_confirmed_email_headers` – 过滤用户请求确认电子邮件的标头。
- `user_request_confirmed_email_subject` – 过滤用户请求确认电子邮件的主题。
- `user_request_confirmed_email_to` – 过滤数据请求确认通知的收件人。
- `user_request_key_expiration` – 过滤用户请求确认密钥的过期时间。
- `wp_privacy_additional_user_profile_data` – 用于扩展隐私导出器的用户个人资料数据的过滤器。
- `wp_privacy_export_expiration` – 控制导出文件允许保留的时间，默认为 3 天。
- `wp_privacy_personal_data_email_content` – 允许修改发送给用户的包含其个人数据导出文件链接的电子邮件消息。
- `wp_privacy_personal_data_email_headers` – 过滤随个人数据导出文件发送的电子邮件的标头。
- `wp_privacy_personal_data_email_subject` – 过滤导出请求完成时发送的电子邮件的主题。
- `wp_privacy_personal_data_email_to` – 过滤个人数据导出电子邮件通知的收件人。

[info]使用 `wp_privacy_personal_data_email_to` 时应格外谨慎，以避免将数据导出链接发送到错误的收件人电子邮件地址。[/info]

- `wp_privacy_personal_data_erasers` – 支持注册核心和插件个人数据擦除器。
- `wp_privacy_personal_data_erasure_page` – 过滤个人数据擦除器数据页面。允许除 AJAX 外，其他目的地也能使用擦除响应。
- `wp_privacy_personal_data_exporters` – 支持注册核心和插件个人数据导出器。
- `wp_privacy_personal_data_export_page` – 过滤个人数据导出器数据页面。用于构建导出报告。允许除 AJAX 外，其他目的地也能使用导出响应。
- `wp_privacy_anonymize_data` – 过滤每种类型的匿名数据。
- `wp_privacy_exports_dir` – 过滤用于存储个人数据导出文件的目录。
- `wp_privacy_exports_url` – 过滤用于存储个人数据导出文件的目录 URL。
- `user_confirmed_action_email_content` – 过滤用户请求确认邮件的正文。当用户请求被确认时，该邮件会发送给管理员。
- `user_erasure_fulfillment_email_to` – 过滤数据擦除完成通知的收件人。
- `user_erasure_complete_email_subject` – 过滤擦除请求完成时发送的邮件主题。
- `user_confirmed_action_email_content` – 过滤数据擦除完成通知的正文。当管理员完成用户的数据擦除请求时，该邮件会发送给用户。
- `user_erasure_complete_email_headers` – 过滤数据擦除完成通知的邮件头。

## 功能权限

隐私工具的访问权限由几个新增功能控制。管理员（在非多站点安装中）默认拥有这些权限。具体权限如下：

- `erase_others_personal_data` – 控制“工具”菜单下是否显示“擦除个人数据”子菜单
- `export_others_personal_data` – 控制“工具”菜单下是否显示“导出个人数据”子菜单  
- `manage_privacy_options` – 控制“设置”菜单下是否显示“隐私”子菜单

# Index

为网站隐私政策提供建议文本
-------------------------------------------

每个收集、使用或存储用户数据，或将其传递给外部来源或第三方的插件，都应在隐私政策文本框中添加建议文本部分。最佳实现方式是使用 `wp_add_privacy_policy_content( $plugin_name, $policy_text )`。这将允许网站管理员将这些信息整合到其网站的隐私政策中。

为方便用户使用，建议文本应涵盖默认隐私政策中提出的问题：

- 我们收集哪些个人数据及收集原因
	- 用户自行输入的信息
	- WP：联系表单
	- WP：评论
	- WP：Cookie
	- WP：第三方嵌入内容
	- 分析数据
- 我们与谁共享您的数据
- 我们保留您的数据多长时间
- 您对数据拥有哪些权利
- 我们将数据发送至何处
- 您的联系信息
- 我们如何保护您的数据
- 我们有哪些数据泄露处理程序
- 我们从哪些第三方接收数据
- 我们如何使用用户数据进行自动化决策和/或画像分析
- 任何行业监管披露要求

虽然并非所有问题都适用于所有插件，但我们建议特别关注数据共享相关部分。

## 代码示例

[info]建议在 [admin_init](https://developer.wordpress.org/reference/hooks/admin_init/) 操作期间调用 [`wp_add_privacy_policy_content()`](https://developer.wordpress.org/reference/functions/wp_add_privacy_policy_content/)。在操作钩子外调用可能导致问题，详情参见工单 [#44142](https://core.trac.wordpress.org/ticket/44142)。[/info]

[info]可通过专用 `.privacy-policy-tutorial` CSS 类提供补充信息。当复制章节内容时，应用了此 CSS 类的 HTML 元素内的所有内容都将从剪贴板中省略。[/info]

```
/**
 * 添加隐私政策声明。
 */
function wporg_add_privacy_policy_content() {
  if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
    return;
  }
  $content = '<p class="privacy-policy-tutorial">' . __( '建议文本的引导性内容。', 'text-domain' ) . '</p>'
  . '<strong class="privacy-policy-tutorial">' . __( '建议文本：', 'my_plugin_textdomain' ) . '</strong> '
  . sprintf(
    __( '当您在本站留言时，我们会将您的姓名、邮箱地址、IP地址和评论文本发送至 example.com。Example.com 不会保留您的个人数据。Example.com 的隐私政策请访问<a href="%1$s" target="_blank">此处</a>。', 'text-domain' ),
    'https://example.com/privacy-policy'
  );
  wp_add_privacy_policy_content( 'Example Plugin', wp_kses_post( wpautop( $content, false ) ) );
}

add_action( 'admin_init', 'wporg_add_privacy_policy_content' );
```

# REST API

WordPress 4.4 引入了 REST API 的基础架构。REST API 提供了一种简便的方式来获取和存储 WordPress 数据。通过向 REST API 服务器发送 HTTP 请求，可以检索和存储数据。REST API 利用了不同的 HTTP 方法。

- `GET` 应用于从 API 检索数据。
- `POST` 应用于创建新资源（例如用户、文章、分类法）。
- `PUT` 应用于更新资源。
- `DELETE` 应用于删除资源。
- `OPTIONS` 应用于提供资源的上下文信息。

资源是任何单一的实体或对象。WordPress 中一个很好的资源示例是文章。文章具有不同的属性，如标题和内容。API 的响应可以将标题和内容作为响应中的字段显示。REST API 使我们能够以新的方式与文章和其他 WordPress 资源进行交互。REST API 使得与网络其他部分共享内容变得更加容易，并提供了处理 WordPress 内复杂交互的结构化方式。

在本插件手册的章节中，我们将探讨 API 的工作原理，以及如何利用其功能在 WordPress 中实现出色的功能！

# 控制器类

## 概述

编写端点时，使用控制器类来处理端点功能会很有帮助。控制器类将提供与 API 交互的标准方式，以及更易于维护的交互方式。WordPress 当前最低 PHP 版本为 5.2，如果您开发的端点将被整个 WordPress 生态系统使用，应考虑支持 WordPress 的最低要求。

PHP 5.2 本身不支持命名空间。这意味着您声明的每个函数都将位于全局作用域中。如果您决定为端点使用通用函数名（如 `get_items()`），而另一个插件也注册了该函数，PHP 将因致命错误而失败。这是因为函数 `get_items()` 被重复声明。通过封装端点，我们可以避免这些命名冲突，并保持与 API 交互的一致性。

## Controllers

Controllers typically do one thing; they receive input, and generate output. For the WordPress REST API our controllers will handle request input as `WP_REST_Request` objects and generate response output as `WP_REST_Response` objects. Let’s look at an example controller class:

```
class My_REST_Posts_Controller {

	// Here initialize our namespace and resource name.
	public function __construct() {
		$this->namespace     = '/my-namespace/v1';
		$this->resource_name = 'posts';
	}

	// Register our routes.
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->resource_name, array(
			// Here we register the readable endpoint for collections.
			array(
				'methods'   => 'GET',
				'callback'  => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
			),
			// Register our schema callback.
			'schema' => array( $this, 'get_item_schema' ),
		) );
		register_rest_route( $this->namespace, '/' . $this->resource_name . '/(?P<id>[\d]+)', array(
			// Notice how we are registering multiple endpoints the 'schema' equates to an OPTIONS request.
			array(
				'methods'   => 'GET',
				'callback'  => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
			),
			// Register our schema callback.
			'schema' => array( $this, 'get_item_schema' ),
		) );
	}

	/**
	 * Check permissions for the posts.
	 *
	 * @param WP_REST_Request $request Current request.
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! current_user_can( 'read' ) ) {
			return new WP_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) );
		}
		return true;
	}

	/**
	 * Grabs the five most recent posts and outputs them as a rest response.
	 *
	 * @param WP_REST_Request $request Current request.
	 */
	public function get_items( $request ) {
		$args = array(
			'post_per_page' => 5,
		);
		$posts = get_posts( $args );

		$data = array();

		if ( empty( $posts ) ) {
			return rest_ensure_response( $data );
		}

		foreach ( $posts as $post ) {
			$response = $this->prepare_item_for_response( $post, $request );
			$data[] = $this->prepare_response_for_collection( $response );
		}

		// Return all of our comment response data.
		return rest_ensure_response( $data );
	}

	/**
	 * Check permissions for the posts.
	 *
	 * @param WP_REST_Request $request Current request.
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! current_user_can( 'read' ) ) {
			return new WP_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) );
		}
		return true;
	}

/**
	 * Grabs the five most recent posts and outputs them as a rest response.
	 *
	 * @param WP_REST_Request $request Current request.
	 */
	public function get_item( $request ) {
		$id = (int) $request['id'];
		$post = get_post( $id );

		if ( empty( $post ) ) {
			return rest_ensure_response( array() );
		}

		$response = prepare_item_for_response( $post );

		// Return all of our post response data.
		return $response;
	}

	/**
	 * Matches the post data to the schema we want.
	 *
	 * @param WP_Post $post The comment object whose response is being prepared.
	 */
	public function prepare_item_for_response( $post, $request ) {
		$post_data = array();

		$schema = $this->get_item_schema( $request );

		// We are also renaming the fields to more understandable names.
		if ( isset( $schema['properties']['id'] ) ) {
			$post_data['id'] = (int) $post->ID;
		}

		if ( isset( $schema['properties']['content'] ) ) {
			$post_data['content'] = apply_filters( 'the_content', $post->post_content, $post );
		}

		return rest_ensure_response( $post_data );
	}

	/**
	 * Prepare a response for inserting into a collection of responses.
	 *
	 * This is copied from WP_REST_Controller class in the WP REST API v2 plugin.
	 *
	 * @param WP_REST_Response $response Response object.
	 * @return array Response data, ready for insertion into collection data.
	 */
	public function prepare_response_for_collection( $response ) {
		if ( ! ( $response instanceof WP_REST_Response ) ) {
			return $response;
		}

		$data = (array) $response->get_data();
		$server = rest_get_server();

		if ( method_exists( $server, 'get_compact_response_links' ) ) {
			$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
		} else {
			$links = call_user_func( array( $server, 'get_response_links' ), $response );
		}

		if ( ! empty( $links ) ) {
			$data['_links'] = $links;
		}

		return $data;
	}

	/**
	 * Get our sample schema for a post.
	 *
	 * @param WP_REST_Request $request Current request.
	 */
	public function get_item_schema( $request ) {
		$schema = array(
			// This tells the spec of JSON Schema we are using which is draft 4.
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
			// The title property marks the identity of the resource.
			'title'                => 'post',
			'type'                 => 'object',
			// In JSON Schema you can specify object properties in the properties attribute.
			'properties'           => array(
				'id' => array(
					'description'  => esc_html__( 'Unique identifier for the object.', 'my-textdomain' ),
					'type'         => 'integer',
					'context'      => array( 'view', 'edit', 'embed' ),
					'readonly'     => true,
				),
				'content' => array(
					'description'  => esc_html__( 'The content for the object.', 'my-textdomain' ),
					'type'         => 'string',
				),
			),
		);

		return $schema;
	}

// 为授权设置正确的 HTTP 状态码。
	public function authorization_status_code() {

		$status = 401;

		if ( is_user_logged_in() ) {
			$status = 403;
		}

		return $status;
	}
}

// 用于从控制器注册新路由的函数。
function prefix_register_my_rest_routes() {
	$controller = new My_REST_Posts_Controller();
	$controller->register_routes();
}

add_action( 'rest_api_init', 'prefix_register_my_rest_routes' );
```

## 概述与未来展望

控制器类在开发端点时为我们解决了两个重大问题：命名空间缺失和结构不一致。需要注意的是，不应滥用端点的继承机制。例如：如果您为文章端点编写了控制器类（如上例所示），并希望同时支持自定义文章类型，**不应**像这样扩展 `My_REST_Posts_Controller`：`class My_CPT_REST_Controller extends My_REST_Posts_Controller`。

正确的做法是：要么创建完全独立的控制器类，要么让 `My_REST_Posts_Controller` 处理所有可用的文章类型。当您开始深入继承机制的黑暗深渊时，必须明白：如果父类在任何时候需要修改，而您的子类又依赖于它们，将会带来巨大麻烦。多数情况下，建议创建基础控制器类作为 `接口` 或 `抽象类`，供各个端点控制器实现或扩展。WP REST API 团队正采用 `抽象类` 方案开发 `WP_REST_Controller` 类，以期未来能纳入核心代码。

目前，支持文章、文章类型、文章状态、修订版本、分类法、术语、用户、评论及附件/媒体资源的“核心端点”正在功能插件中开发，有望在未来融入 WordPress 核心。该插件内包含一个提案中的 `WP_REST_Controller` 类，可用于为您的端点构建专属控制器。`WP_REST_Controller` 具备诸多优势，为 API 端点创建提供了统一规范的方法。

# 请求

## 概述

REST API 在许多方面都非常简单。存在输入，即请求。服务器解释输入并生成输出，即响应。在某些方面，您可以将对 WordPress REST API 的请求视为一组应由 API 执行和解释的指示或指令。默认情况下，WordPress REST API 旨在使用 HTTP 请求作为其请求媒介。HTTP 是互联网上数据通信的基础，这使得 WordPress REST API 成为一个影响深远的 API。API 中的请求利用了 HTTP 请求中的许多不同方面，如 URI、HTTP 方法、标头和参数。请求的数据结构由 `WP_REST_Request` 类方便地处理。

## WP_REST_Request

该类是 WordPress 4.4 引入的三个核心基础设施类之一。当向 API 端点发起 HTTP 请求时，API 会自动创建 `WP_REST_Request` 类的实例，并与提供的数据进行匹配。响应对象在 `WP_REST_Server` 的 `serve_request()` 方法中自动生成。请求创建并完成身份验证后，请求被分派，我们注册的端点回调函数开始执行。存储在 `WP_REST_Request` 对象中的所有数据都会传递给我们注册端点的回调函数。因此，`permissions_callback` 和 `callback` 都会接收到传入的请求对象。这使得我们能够在回调函数中访问各种请求属性，从而根据期望的输出定制响应内容。

## 请求属性

请求对象具有许多不同的属性，每个属性都可以以各种方式使用。主要属性包括请求方法、路由、标头、参数和属性。让我们逐一分析这些属性在请求中的作用。如果您要自己创建请求对象，它将如下所示：

```
$request = new WP_REST_Request( 'GET', '/my-namespace/v1/examples' );
```

在上面的代码示例中，我们仅指定请求对象方法为 `GET`，并且应匹配路由 `/my-namespace/v1/examples`，在整个 URL 上下文中将如下所示：`https://ourawesomesite.com/wp-json/my-namepsace/v1/examples`。`WP_REST_Request` 构造函数的 method 和 route 参数用于将请求映射到所需的端点。如果请求发送到未注册的端点，则响应中将返回有用的 404 错误消息。让我们更深入地了解各种属性。

### 方法

请求对象的 method 属性默认与 HTTP 请求方法匹配。在大多数情况下，方法将是 `GET`、`POST`、`PUT`、`DELETE`、`OPTIONS` 或 `HEAD` 之一。这些方法将用于匹配注册到路由的各个端点。当 API 找到方法与路由的匹配项时，它将触发该端点的回调函数。

以下约定是匹配 HTTP 方法的最佳实践：`GET` 用于只读任务，`POST` 用于创建，`PUT` 用于更新，`DELETE` 用于删除。请求方法充当端点预期功能的指示器。当您向路由发出 `GET` 请求时，应期望返回只读数据。

### 路由

请求的路由默认会匹配服务器环境变量中的路径信息：`$_SERVER['PATH_INFO']`。当你向 WordPress REST API 的某个路由发起 HTTP 请求时，生成的 `WP_REST_Request` 对象将与该路径匹配，并期望最终匹配到有效的端点。简而言之，请求的路由就是你在 API 中希望定位的目标位置。

如果我们使用 `GET` 方法注册了一个书籍端点，它可能位于 `https://ourawesomesite.com/wp-json/my-namespace/v1/books`。在浏览器中访问该 URL，我们将看到以 JSON 格式呈现的书籍集合。WordPress 会自动为我们生成请求对象，并处理所有路由匹配端点的逻辑。因此，既然我们无需自行处理路由，理解如何在请求中传递额外数据就成了更重要的学习内容。

### 请求头

HTTP 请求头本质上是关于我们 HTTP 请求的附加数据。请求头可以指定缓存策略、请求内容、请求来源等诸多信息。请求头不一定直接与我们的端点交互，但其中的信息能帮助 WordPress 确定如何处理请求。若需传递端点需要交互的数据，我们应当使用参数。

### 参数

向 WordPress REST API 发起请求时，传递的大部分附加数据都将以参数形式呈现。什么是参数？在 API 上下文中存在四种不同类型：路由参数、查询参数、正文参数和文件参数。让我们逐一深入探讨。

#### URL Params

URL parameters are automatically generated in a `WP_REST_Request` from the path variables in the requested route. What does that mean? Let’s look at this route, which grabs individual books by id: `/my-namespace/v1/books/(?P\d+)`. The odd looking `(?P\d+)` is a path variable. The name of the path variable is ‘`id`‘.

If we were to make a request like `GET https://ourawesomesite.com/wp-json/my-namespace/v1/books/5`, `5` will become the value for our `id` path variable. The `WP_REST_Request` object will automatically take that path variable and store it as a URL parameter. Now inside of our endpoint callbacks we can interact with that URL parameter really easily. Let’s look at an example.  

```
// Register our individual books endpoint.
function prefix_register_book_route() {
    register_rest_route( 'my-namespace/v1', '/books/(?P<id>\d+)', array(
        // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
        'methods' => WP_REST_Server::READABLE,
        // Register the callback for the endpoint.
        'callback' => 'prefix_get_book',
    ) );
}

add_action( 'rest_api_init', 'prefix_register_book_route' ); 

/**
 * Our registered endpoint callback. Notice how we are passing in $request as an argument.
 * By default, the WP_REST_Server will pass in the matched request object to our callback.
 *
 * @param WP_REST_Request $request The current matched request object.
 */
function prefix_get_book( $request ) {
    // Here we are accessing the path variable 'id' from the $request.
    $book = prefix_get_the_book( $request['id'] );
    return rest_ensure_response( $book );
}

// A simple function that grabs a book title from our books by ID.
function prefix_get_the_book( $id ) {
    $books = array(
        'Design Patterns',
        'Clean Code',
        'Refactoring',
        'Structure and Interpretation of Computer Programs',
    );

    $book = '';
    if ( isset( $books[ $id ] ) ) {
        // Grab the matching book.
        $book = $books[ $id ];
    } else {
        // Error handling.
        return new WP_Error( 'rest_not_found', esc_html__( 'The book does not exist', 'my-text-domain' ), array( 'status' => 404 ) );
    }

    return $book;
}
```

In the example above we see how path variables are stored as URL parameters in the request object. We can then access those parameters in our endpoint callbacks. The above example is a pretty common use case for using URL params. Adding too many path variables to a route can slow down the matching of routes and it can also over complicate registering endpoints, it is advised to use URL parameters sparingly. If we aren’t supposed to use parameters directly in our URL path, then we need another way to pass in extra information to our request. This is where query and body parameters come in, they will typically do most of the heavy lifting in your API.

#### 查询参数

查询参数存在于 URI 的查询字符串部分。在 `https://ourawesomesite.com/wp-json/my-namespace/v1/books?per_page=2&genre=fiction` 中，URI 的查询字符串部分是 `?per_page=2&genre=fiction`。查询字符串以 '`?`' 字符开始，查询字符串中的不同值由 '`&`' 字符分隔。我们在查询字符串中指定了两个参数：`per_page` 和 `fiction`。在我们的端点中，我们可能只想获取属于小说类别的两本书。我们可以在回调函数中这样访问这些值：`$request['per_page']` 和 `$request['genre']`（假设 `$request` 是我们使用的参数名称）。如果你熟悉 PHP，你可能已经在你的 Web 应用程序中使用过查询参数。

在 PHP 中，查询参数存储在超全局变量 `$_GET` 中。需要注意的是，你永远不应该直接在端点中访问任何超全局变量或服务器变量。最好始终使用 `WP_REST_Request` 类提供的内容。另一种向端点传递变量的常见方法是使用正文参数。

#### 请求体参数

请求体参数是以键值对形式存储在请求体中的数据。如果你曾通过 HTML 表单、cURL 或其他方式发送过 `POST` 请求，那么你就已经使用过请求体参数。请求体参数可以通过不同的内容类型进行传递。`POST` 请求默认的 `Content-Type` 请求头是 `x-www-form-urlencoded`。当使用 `x-www-form-urlencoded` 时，参数会以查询字符串的形式发送，例如 `per_page=2&genre=fiction`。默认情况下，HTML 表单会将各种输入字段打包，并发送符合 `x-www-form-urlencoded` 模式的 `POST` 请求。

需要注意的是，虽然 HTTP 规范并未禁止在 `GET` 请求中使用请求体参数，但建议不要在 `GET` 请求中使用。请求体参数可以且应该用于 `POST`、`PUT` 和 `DELETE` 请求。

#### 文件参数

当请求使用特殊内容类型标头 `multipart/form-data` 时，`WP_REST_Request` 对象中的文件参数会被存储。随后可通过 `$request->get_file_params()` 从请求对象访问文件数据。文件参数等同于 PHP 超全局变量：`$_FILES`。请记住，不要直接访问超全局变量，仅使用 `WP_REST_Request` 对象提供的方法。

在端点回调中，我们可以使用 `wp_handle_upload()` 将所需文件添加到 WordPress 的媒体上传目录。文件参数仅适用于处理文件数据，切勿将其用于任何其他目的。

### 属性

`WP_REST_Request` 同样支持请求属性。请求属性是指注册到匹配路由的属性。如果我们向 `my-namespace/v1/books` 发起 `GET` 请求，然后在端点回调中调用 `$request->get_attributes()`，我们将获得 `my-namespace/v1/books` 端点的所有注册选项。如果我们向同一路由发起 `POST` 请求，且端点回调同样返回 `$request->get_attributes()`，则会收到注册到 `POST` 端点回调的另一组端点选项。

在属性中，我们将获得包含以下内容的响应：支持的方法、选项、是否在索引中显示此端点、端点已注册参数列表以及已注册的回调函数。其返回内容可能如下所示：

```
{
  "methods": {
    "GET": true
  },
  "accept_json": false,
  "accept_raw": false,
  "show_in_index": true,
  "args": {
    "context": {
      "description": "Scope under which the request is made; determines fields present in response.",
      "type": "string",
      "sanitize_callback": "sanitize_key",
      "validate_callback": "rest_validate_request_arg",
      "enum": [
        "view",
        "embed",
        "edit"
      ],
      "default": "view"
    },
    "page": {
      "description": "Current page of the collection.",
      "type": "integer",
      "default": 1,
      "sanitize_callback": "absint",
      "validate_callback": "rest_validate_request_arg",
      "minimum": 1
    },
    "per_page": {
      "description": "Maximum number of items to be returned in result set.",
      "type": "integer",
      "default": 10,
      "minimum": 1,
      "maximum": 100,
      "sanitize_callback": "absint",
      "validate_callback": "rest_validate_request_arg"
    },
    "search": {
      "description": "Limit results to those matching a string.",
      "type": "string",
      "sanitize_callback": "sanitize_text_field",
      "validate_callback": "rest_validate_request_arg"
    },
    "after": {
      "description": "Limit response to resources published after a given ISO8601 compliant date.",
      "type": "string",
      "format": "date-time",
      "validate_callback": "rest_validate_request_arg"
    },
    "author": {
      "description": "Limit result set to posts assigned to specific authors.",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list",
      "validate_callback": "rest_validate_request_arg"
    },
    "author_exclude": {
      "description": "Ensure result set excludes posts assigned to specific authors.",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list",
      "validate_callback": "rest_validate_request_arg"
    },
    "before": {
      "description": "Limit response to resources published before a given ISO8601 compliant date.",
      "type": "string",
      "format": "date-time",
      "validate_callback": "rest_validate_request_arg"
    },
    "exclude": {
      "description": "Ensure result set excludes specific ids.",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list"
    },
    "include": {
      "description": "Limit result set to specific ids.",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list"
    },
    "offset": {
      "description": "Offset the result set by a specific number of items.",
      "type": "integer",
      "sanitize_callback": "absint",
      "validate_callback": "rest_validate_request_arg"
    },
    "order": {
      "description": "Order sort attribute ascending or descending.",
      "type": "string",
      "default": "desc",
      "enum": [
        "asc",
        "desc"
      ],
      "validate_callback": "rest_validate_request_arg"
    },
    "orderby": {
      "description": "Sort collection by object attribute.",
      "type": "string",
      "default": "date",
      "enum": [
        "date",
        "relevance",
        "id",
        "include",
        "title",
        "slug"
      ],
      "validate_callback": "rest_validate_request_arg"
    },
    "slug": {
      "description": "Limit result set to posts with a specific slug.",
      "type": "string",
      "validate_callback": "rest_validate_request_arg"
    },
    "status": {
      "default": "publish",
      "description": "Limit result set to posts assigned a specific status; can be comma-delimited list of status types.",
      "enum": [
        "publish",
        "future",
        "draft",
        "pending",
        "private",
        "trash",
        "auto-draft",
        "inherit",
        "any"
      ],
      "sanitize_callback": "sanitize_key",
      "type": "string",
      "validate_callback": [
        {},
        "validate_user_can_query_private_statuses"
      ]
    },
    "filter": {
      "description": "Use WP Query arguments to modify the response; private query vars require appropriate authorization."
    },
    "categories": {
      "description": "Limit result set to all items that have the specified term assigned in the categories taxonomy.",
      "type": "array",
      "sanitize_callback": "wp_parse_id_list",
      "default": []
    },
    "tags": {
      "description": "Limit result set to all items that have the specified term assigned in the tags taxonomy.",
      "type": "array",
      "sanitize_callback": "wp_parse_id_list",
      "default": []
    }
  },
  "callback": [
    {},
    "get_items"
  ],
  "permission_callback": [
    {},
    "get_items_permissions_check"
  ]
}
```

如您所见，我们已将所有注册到端点的信息准备就绪，随时可用！请求属性通常在较低层级使用，由 `WP_REST_Server` 类处理，但在端点回调内部仍可进行一些有趣的操作，例如将接受的参数限制为与注册参数匹配。

WP REST API 的设计旨在让您无需处理任何内部细节，因此这些与 `WP_REST_Request` 交互的高级方法并不常用。使用 WP REST API 的核心在于注册路由和端点。请求是我们用来告知 API 要访问哪个端点的工具，这通常通过 HTTP 完成，但我们也可以在内部使用 `WP_REST_Request`。

## Internal Requests

The key to making internal requests is using `rest_do_request()`. All you need to do is pass in a request object and you will be returned a response. Because the request is never served by the `WP_REST_Server`, the response data is never encoded into json, meaning we have our response object as a PHP object. This is pretty awesome and enables us to do a lot of interesting things. For one, we can create efficient batch endpoints. From a performance perspective, one of the hurdles is minimizing HTTP requests. We can create batch endpoints that will use `rest_do_request()` to serve all of our requests internally all in one HTTP request. Here is a very simplistic batch endpoint for read only data, so you can see `rest_do_request()` in action.

```
// Register our mock batch endpoint.
function prefix_register_batch_route() {
    register_rest_route( 'my-namespace/v1', '/batch', array(
        // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
        'methods' => WP_REST_Server::READABLE,
        // Register the callback for the endpoint.
        'callback' => 'prefix_do_batch_request',
        // Register args for the batch endpoint.
        'args' => prefix_batch_request_parameters(),
    ) );
}

add_action( 'rest_api_init', 'prefix_register_batch_route' );

/**
 * Our registered endpoint callback. Notice how we are passing in $request as an argument.
 * By default, the WP_REST_Server will pass in the matched request object to our callback.
 *
 * @param WP_REST_Request $request The current matched request object.
 */
function prefix_do_batch_request( $request ) {
    // Here we initialize the array that will hold our response data.
    $data = array();
	$data = prefix_handle_batch_requests( $request['requests'] );
	return $data;
}

/**
 * This handles the building of the response for the batch requests we make.
 *
 * @param array $requests An array of data to build WP_REST_Request objects from.
 * @return WP_REST_Response A collection of response data for batch endpoints.
 */
function prefix_handle_batch_requests( $requests ) {
	$data = array();

	// Foreach request specified in the requests param run the endpoint.
	foreach ( $requests as $request_params ) {
		$response = prefix_handle_request( $request_params );
		$key = $request_params['method'] . ' ' . $request_params['route'];
		$data[ $key ] = prefix_prepare_for_collection( $response );
	}

	return rest_ensure_response( $data );
}

/**
 * This handles the building of the response for the batch requests we make.
 *
 * @param array $request_params Data to build a WP_REST_Request object from.
 * @return WP_REST_Response Response data for the request.
 */
function prefix_handle_request( $request_params ) {
	$request = new WP_REST_Request( $request_params['method'], $request_params['route'] );

// Add specified request parameters into the request.
	if ( isset( $request_params['params'] ) ) {
		foreach ( $request_params['params'] as $param_name => $param_value ) {
			$request->set_param( $param_name, $param_value );
		}
	}
	$response = rest_do_request( $request );
	return $response;
}

/**
 * Prepare a response for inserting into a collection of responses.
 *
 * This is lifted from WP_REST_Controller class in the WP REST API v2 plugin.
 *
 * @param WP_REST_Response $response Response object.
 * @return array Response data, ready for insertion into collection data.
 */
function prefix_prepare_for_collection( $response ) {
	if ( ! ( $response instanceof WP_REST_Response ) ) {
		return $response;
	}

	$data = (array) $response->get_data();
	$server = rest_get_server();

	if ( method_exists( $server, 'get_compact_response_links' ) ) {
		$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
	} else {
		$links = call_user_func( array( $server, 'get_response_links' ), $response );
	}

	if ( ! empty( $links ) ) {
		$data['_links'] = $links;
	}

	return $data;
}

/**
 * Returns the JSON schema data for our registered parameters.
 *
 * @return array $params A PHP representation of JSON Schema data.
 */
function prefix_batch_request_parameters() {
    $params = array();

    $params['requests'] = array(
		'description'        => esc_html__( 'An array of request objects arguments that can be built into WP_REST_Request instances.', 'my-text-domain' ),
		'type'               => 'array',
		'required'           => true,
		'validate_callback'  => 'prefix_validate_requests',
		'items'              => array(
			array(
				'type' => 'object',
				'properties' => array(
					'method' => array(
						'description' => esc_html__( 'HTTP Method of the desired request.', 'my-text-domain' ),
						'type'        => 'string',
						'required'    => true,
						'enum'        => array(
							'GET',
							'POST',
							'PUT',
							'DELETE',
							'OPTIONS',
						),
					),
					'route' => array(
						'description' => esc_html__( 'Desired route for the request.', 'my-text-domain' ),
						'required'    => true,
						'type'        => 'string',
						'format'      => 'uri',
					),
					'params' => array(
						'description' => esc_html__( 'Key value pairs of desired request parameters.', 'my-text-domain' ),
						'type' => 'object',
					),
				),
			),
		),
	);

	return $params;
}

function prefix_validate_requests( $requests, $request, $param_key ) {
	// If requests isn't an array of requests then we don't process the batch.
	if ( ! is_array( $requests ) ) {
		return new WP_Error( 'rest_invald_param', esc_html__( 'The requests parameter must be an array of requests.' ), array( 'status' => 400 ) );
	}

foreach ( $requests as $request ) {
		// If the method or route is not set then we do not run the requests.
		if ( ! isset( $request['method'] ) || ! isset( $request['route'] ) ) {
			return new WP_Error( 'rest_invald_param', esc_html__( 'You must specify the method and route for each request.' ), array( 'status' => 400 ) );
		}

		if ( isset( $request['params'] ) && ! is_array( $request['params'] ) ) {
			return new WP_Error( 'rest_invald_param', esc_html__( 'You must specify the params for each request as an array of named key value pairs.' ), array( 'status' => 400 ) );
		}
	}

	// This is a black listing approach to data validation.
	return true;
}
```

That is quite a decent chunk of code that covers a number of topics, but everything centers around what happens in `prefix_handle_request()`. Here we are passing in an array that tells us a HTTP method, a route, and a set of parameters we want to turn into a request. We then build the request object for the method and route. If any parameters were specified we use the `WP_REST_Request::set_param()` method to add in the desired parameters. Once our `WP_REST_Request` is ready to go we use `rest_do_request` to internally match that endpoint and the response is returned to our batch endpoint response collection. Using a batch endpoint like this can net you huge performance gains, as you will only make one HTTP request to get a response for multiple endpoints. The implementation of this is not necessarily the best and serves as an example; not the only way to do this.

# 响应

## 概述

API 中的响应承载了我们所需的所有数据。如果我们的请求有误，响应数据也应告知我们发生了错误。WordPress REST API 中的响应应返回我们请求的数据或错误信息。API 中的响应由 `WP_REST_Response` 类处理，这是 API 的三个基础类之一。

## WP_REST_Response

`WP_REST_Response` 继承自 WordPress 的 `WP_HTTP_Response` 类，允许我们访问响应头、响应状态码和响应数据。

```
// 以下代码仅作演示，不会执行任何操作。
$response = new WP_REST_Response( '这是一些数据' );

// 要获取响应数据，我们可以使用此方法。它应等于'这是一些数据'。
$our_data = $response->get_data();

// 要访问 HTTP 状态码，我们可以使用此方法。最常见的状态码可能是 200，表示成功！
$our_status = $response->get_status();

// 要访问 HTTP 响应头，我们可以使用此方法。
$our_headers = $response->get_headers();
```

以上内容相当直接，展示了如何从响应中获取所需信息。`WP_REST_Response` 更进一步。您可以通过 `$response->get_matched_route()` 访问响应匹配的路由，以追溯响应来自哪个端点。`$response->get_matched_handler()` 将返回生成我们响应的端点所注册的选项。这些功能对于记录 API 日志等用途可能很有用。响应类还帮助我们进行错误处理。

### Error Handling

If something went terribly wrong in our request, we can return `WP_Error` objects in our endpoint callbacks explaining what went wrong, like this:

```
// Register our mock batch endpoint.
function prefix_register_broken_route() {
    register_rest_route( 'my-namespace/v1', '/broken', array(
        // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
        'methods' => WP_REST_Server::READABLE,
        // Register the callback for the endpoint.
        'callback' => 'prefix_get_an_error',
    ) );
}

add_action( 'rest_api_init', 'prefix_register_broken_route' );

/**
 * Our registered endpoint callback. Notice how we are passing in $request as an argument.
 * By default, the WP_REST_Server will pass in the matched request object to our callback.
 *
 * @param WP_REST_Request $request The current matched request object.
 */
function prefix_get_an_error( $request ) {
    return new WP_Error( 'oops', esc_html__( 'This endpoint is currently broken, try another endpoint, I promise the API is cool! EEEK!!!!', 'my-textdomain' ), array( 'status' => 400 ) );
}
```

That is kind of a silly example but it touches on some key things. The most important thing to understand is that the WordPress REST API will automatically handle changing the [WP\_Error](https://developer.wordpress.org/reference/classes/wp_error/) object into an HTTP Response containing your data. When you set the status code in the `WP_Error` object your HTTP response status code will take on that value. This comes in really handy when you need to use different error codes like 404 for content that wasn’t found, or 403 for forbidden access. All we have to do is have our endpoint callbacks return a request and the `WP_REST_Server` class will handle a lot of really important things for us.

There are other cool things the response class can help us with, like Linking.

## Linking

What if we wanted to get a post and the first comment for that post? Would we write a separate endpoint to handle this use case? If we did that, we would need to start adding a lot of endpoints to handle various small use cases and our API index would get bloated really fast. Response Linking provides us a way to form relations between our resources that the API can understand. The API implements a standard known as HAL for resource linking. Let’s look at our post and comment example, it would be better to have routes for each resource.

Let’s say we have post with ID = 1 and comment ID = 3. The comment is assigned to post 1, so realistically the two resources could live at the routes `/my-namespace/v1/posts/1` and `/my-namespace/v1/comments/3`. We would add links to the responses to create the relationships between them. Let’s look at this from the comment perspective first.

```
// Register our mock endpoints.
function prefix_register_my_routes() {
    register_rest_route( 'my-namespace/v1', '/posts/(?P<id>[\d]+)', array(
        // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
        'methods' => WP_REST_Server::READABLE,
        // Register the callback for the endpoint.
        'callback' => 'prefix_get_rest_post',
    ) );
    register_rest_route( 'my-namespace/v1', '/comments', array(
        // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
        'methods' => WP_REST_Server::READABLE,
        // Register the callback for the endpoint.
        'callback' => 'prefix_get_rest_comments',
        // Register the post argument to limit results to a specific post parent.
        'args' => array(
            'post' => array(
                'description' => esc_html__( 'The post ID that the comment is assigned to.', 'my-textdomain' ),
                'type'        => 'integer',
                'required'    => true,
            ),
        ),
    ) );
    register_rest_route( 'my-namespace/v1', '/comments/(?P<id>[\d]+)', array(
        // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
        'methods' => WP_REST_Server::READABLE,
        // Register the callback for the endpoint.
        'callback' => 'prefix_get_rest_comment',
    ) );
}

add_action( 'rest_api_init', 'prefix_register_my_routes' );

// Grab a post.
function prefix_get_rest_post( $request ) {
    $id = (int) $request['id'];
    $post = get_post( $id );

    $response = rest_ensure_response( array( $post ) );

    $response->add_links( prefix_prepare_post_links( $post ) );

    return $response;
}

// Prepare post links.
function prefix_prepare_post_links( $post ) {
    $links = array();

    $replies_url = rest_url( 'my-namespace/v1/comments' );
    $replies_url = add_query_arg( 'post', $post->ID, $replies_url );
    $links['replies'] = array(
		'href'         => $replies_url,
		'embeddable'   => true,
    );

    return $links;
}

// Grab a comments.
function prefix_get_rest_comments( $request ) {
    if ( ! isset( $request['post'] ) ) {
        return new WP_Error( 'rest_bad_request', esc_html__( 'You must specify the post parameter for this request.', 'my-text-domain' ), array( 'status' => 400 ) );
    }

    $data = array();

    $comments = get_comments( array( 'post__in' => $request['post'] ) );

    if ( empty( $comments ) ) {
        return array();
    }

    foreach( $comments as $comment ) {
        $response = rest_ensure_response( $comment );
        $response->add_links( prefix_prepare_comment_links( $comment ) );
        $data[] = prefix_prepare_for_collection( $response );
    }

    $response = rest_ensure_response( $data );
    return $response;
}

// Grab a comment.
function prefix_get_rest_comment( $request ) {
    $id = (int) $request['id'];
    $post = get_comment( $id );

    $response = rest_ensure_response( $comment );

    $response->add_links( prefix_prepare_comment_links( $comment ) );

    return $response;
}

// Prepare comment links.
function prefix_prepare_comment_links( $comment ) {
    $links = array();
    if ( 0 !== (int) $comment->comment_post_ID ) {
        $post = get_post( $comment->comment_post_ID );
        if ( ! empty( $post->ID ) ) {
        $links['up'] = array(
                'href'       => rest_url( 'my-namespace/v1/posts/' . $comment->comment_post_ID ),
                'embeddable' => true,
                'post_type'  => $post->post_type,
            );
        }
    }
    return $links;
}

/**
 * Prepare a response for inserting into a collection of responses.
 *
 * This is lifted from WP_REST_Controller class in the WP REST API v2 plugin.
 *
 * @param WP_REST_Response $response Response object.
 * @return array Response data, ready for insertion into collection data.
 */
function prefix_prepare_for_collection( $response ) {
	if ( ! ( $response instanceof WP_REST_Response ) ) {
		return $response;
	}

	$data = (array) $response->get_data();
	$server = rest_get_server();

	if ( method_exists( $server, 'get_compact_response_links' ) ) {
		$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
	} else {
		$links = call_user_func( array( $server, 'get_response_links' ), $response );
	}

	if ( ! empty( $links ) ) {
		$data['_links'] = $links;
	}

	return $data;
}
```

如上方示例所示，我们使用链接来建立资源间的关系。若文章存在评论，端点回调将添加指向评论路由的链接，并通过 `post` 参数匹配当前文章ID。访问该路由即可获取关联此文章ID的评论。搜索评论时，每条评论均包含指向文章的 `up` 链接——在遵循HAL规范的链接中，`up` 具有特殊含义：跟随评论的向上链接将返回该评论的父级文章。链接机制已足够出色，但还有更强大的功能。

WordPress REST API 同时支持嵌入功能。请注意我们为两个链接设置的 `embeddable => true` 参数，这使得关联数据可嵌入响应中。例如要获取编号3的评论及其关联文章，可发起请求：`https://ourawesomesite.com/wp-json/my-namespace/v1/comments/3?_embed`。`_embed` 参数指示API将当前请求中所有可嵌入资源链接一并返回。嵌入功能通过单次HTTP请求处理多个资源，显著提升了性能表现。

巧妙运用嵌入与链接机制，使WordPress REST API在交互操作中展现出卓越的灵活性与强大功能。

# REST API 概述

WordPress REST API 为 WordPress 带来了许多新功能。REST API 使用 JSON（JavaScript 对象表示法）作为其数据格式。JSON 是一种开放标准数据格式，在整个网络乃至通用软件中正得到越来越广泛的应用。它轻量且人类可读，看起来像 JavaScript 中的对象；因此得名。当你向 API 发出请求时，响应将以 JSON 格式返回。这使得开发者能够使用 PHP 以外的语言来使用 WordPress，从而允许以新颖而激动人心的方式使用 WordPress。

## 为何使用 WordPress REST API

WordPress REST API 拥有众多应用场景。其中最主要的用途之一是在 WordPress 之上构建单页应用程序。您可以为 WordPress 创建全新的管理界面体验，也可以打造全新的前端交互体验。甚至无需使用 PHP 编写应用程序——任何能够发起 HTTP 请求并解析 JSON 的编程语言都可用于在 WordPress 平台上进行开发。

WordPress REST API 也可作为核心中 admin-ajax API 的强大替代方案。通过使用 REST API，您可以更灵活地构建数据进出 WordPress 的传输结构。借助 REST API 可大幅简化 AJAX 调用，从而在工作中提供更优质的用户体验。

其应用场景远不止于此，真正限制我们的是想象力。关键在于，如果您需要一种结构化、可扩展且简单的方式来传输 WordPress 数据，那么 REST API 很可能是理想选择。尽管该 API 设计简洁，但初期可能显得复杂，我们将尝试将其拆解为小型组件，以便逐步拼凑出完整的应用图景。

## 核心概念

要开始使用 WordPress REST API，我们先来解析与该 API 相关的一些核心概念和术语：

- 路由/端点
- 请求
- 响应
- 模式
- 控制器类

这些概念在理解和使用 WordPress REST API 中都扮演着至关重要的角色。让我们先简要解析它们，以便后续能更深入地探讨每个概念。

### 路由与端点

在 WordPress REST API 中，路由是指可映射到不同 HTTP 方法的 URI。将单个 HTTP 方法映射到路由的过程称为端点。具体来说：当我们向 `http://oursite.com/wp-json/` 发起 `GET` 请求时，会收到 JSON 响应，显示可用路由及每个路由下的可用端点。`/wp-json/` 本身就是一个路由，当发起 `GET` 请求时，会匹配到显示 WordPress REST API 索引的端点。我们将在后续章节学习如何注册自定义路由和端点。

### 请求

在 WordPress REST API 架构中，`WP_REST_Request` 是核心类之一。该类用于存储和检索当前请求的信息，也可在 PHP 内部发起请求以避免使用 HTTP。当向已注册路由发起 HTTP 请求时，系统会自动生成 `WP_REST_Request` 对象。请求中指定的数据将直接影响 API 返回的响应内容。通过请求类可实现诸多实用功能，后续章节将对此展开详细说明。

### 响应

响应是从 API 获取的数据。`WP_REST_Response` 提供了一种与端点返回的响应数据进行交互的方式。响应可以返回所需的数据，也可用于返回错误信息。

### 模式

当我们需要处理不同类型数据的响应和请求时，必须能够识别所交互的数据类型。模式为我们提供了一种结构化数据的方式。模式还为 API 带来了安全优势，因为它使我们能够验证向 API 发出的请求。模式是一个庞大的主题，我们将在模式章节中深入探讨。

### 控制器类

正如您所见，WordPress REST API 包含许多需要协同工作的动态组件。控制器类使我们能够在一个统一的位置整合所有这些元素。通过控制器类，我们将能够管理路由和端点的注册、处理请求、利用模式并生成响应。

## 后续步骤

让我们深入了解如何为 REST API 注册路由和端点。

# Routes & Endpoints

The REST API provides a way to match URIs to various resources in our WordPress installation. By default, if you have pretty permalinks enabled, the WordPress REST API “lives” at `/wp-json/`. At our WordPress site `https://ourawesomesite.com`, we can access the REST API’s index by making a `GET` request to `https://ourawesomesite.com/wp-json/`. The index provides information regarding what routes are available for that particular WordPress install, along with what HTTP methods are supported and what endpoints are registered.

If we wanted to create an endpoint that would return the phrase “Hello World, this is the WordPress REST API”, we would first need to register the route for that endpoint. To register routes you should use the `register_rest_route()` function. It needs to be called on the `rest_api_init` action hook. `register_rest_route()` handles all of the mapping for routes to endpoints. Let’s try to create a “Hello World, this is the WordPress REST API” route.

```
/**
 * This is our callback function that embeds our phrase in a WP_REST_Response
 */
function prefix_get_endpoint_phrase() {
    // rest_ensure_response() wraps the data we want to return into a WP_REST_Response, and ensures it will be properly returned.
    return rest_ensure_response( 'Hello World, this is the WordPress REST API' );
}

/**
 * This function is where we register our routes for our example endpoint.
 */
function prefix_register_example_routes() {
    // register_rest_route() handles more arguments but we are going to stick to the basics for now.
    register_rest_route( 'hello-world/v1', '/phrase', array(
        // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
        'methods'  => WP_REST_Server::READABLE,
        // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
        'callback' => 'prefix_get_endpoint_phrase',
    ) );
}

add_action( 'rest_api_init', 'prefix_register_example_routes' );
```

The first argument passed into `register_rest_route()` is the namespace, which provides us a way to group our routes. The second argument passed in is the resource path, or resource base. For our example, the resource we are retrieving is the “Hello World, this is the WordPress REST API” phrase. The third argument is an array of options. We specify what methods the endpoint can use and what callback should happen when the endpoint is matched (more things can be done but these are the fundamentals).

The third argument also allows us to provide a permissions callback, which can restrict access for the endpoint to only certain users. The third argument also offers a way to register arguments for the endpoint so that requests can modify the response of our endpoint. We will get into those concepts in the endpoints section of this guide.

当我们访问 `https://ourawesomesite.com/wp-json/hello-world/v1/phrase` 时，现在可以看到我们的 REST API 友好地向我们致意。让我们更深入地了解一下路由。

## 路由

REST API 中的路由由 URI 表示。路由本身是附加在 `https://ourawesomesite.com/wp-json` 末尾的部分。API 的索引路由是 `'/'`，这就是为什么 `https://ourawesomesite.com/wp-json/` 会返回 API 的所有可用信息。所有路由都应构建在此路由之上，`wp-json` 部分可以更改，但通常建议保持不变。

我们需要确保路由是唯一的。例如，我们可以为书籍设置这样的路由：`/books`。我们的书籍路由现在将位于 `https://ourawesomesite.com/wp-json/books`。然而，这不是一个好的做法，因为我们会最终污染 API 的潜在路由。如果另一个插件也想注册一个书籍路由怎么办？那样我们会遇到大麻烦，因为两个路由会相互冲突，只能使用其中一个。`register_rest_field()` 的第四个参数是一个布尔值，用于指示路由是否应覆盖现有路由。

覆盖参数也不能真正解决我们的问题，因为两个路由都可能覆盖，或者我们可能希望将两个路由用于不同目的。这就是为路由使用命名空间的原因。

### 命名空间

为路由添加命名空间至关重要。那些等待被合并到 WordPress 核心的“核心”端点使用 `/wp/v2` 命名空间。

[info]**除非你创建端点是为了将其合并到核心中，否则请勿将任何内容放入 `/wp` 命名空间。**[/info]

核心端点命名空间中有一些关键点需要注意。命名空间的第一部分是 `/wp`，代表供应商名称，即 WordPress。对于我们的插件，我们需要为命名空间的供应商部分想一个独特的名称。在上面的例子中，我们使用了 `hello-world`。

供应商部分之后是命名空间的版本部分。“核心”端点使用 `v2` 来代表 WordPress REST API 的版本 2。如果你正在编写一个插件，可以通过简单地创建新端点并提升你提供的版本号来保持 REST API 端点的向后兼容性。这样，原始的 `v1` 和 `v2` 端点都可以被访问。

路由中命名空间之后的部分是资源路径。

### 资源路径

资源路径应体现端点关联的资源类型。在前述示例中，我们使用 `phrase` 一词表示当前交互的资源是短语。为避免命名冲突，每个注册的资源路径在其命名空间内应保持唯一性。资源路径应用于定义特定命名空间内的不同资源路由。

假设某插件处理基础电商功能，我们将涉及订单和商品两类核心资源。订单是对商品的请求，但并非商品本身。同理，商品资源虽与订单相关却本质不同，应分别置于独立的资源路径中。最终电商插件的路由将呈现为：`/my-shop/v1/orders` 与 `/my-shop/v1/products`。

采用此类路由结构时，每个端点应返回对应资源的集合。若需通过ID获取特定商品，则需在路由中使用路径变量。

### Path Variables

Path variables enable us to add dynamic routes. To expand on our eCommerce routes, we could register a route to grab individual products.

```
/**
 * This is our callback function to return our products.
 *
 * @param WP_REST_Request $request This function accepts a rest request to process data.
 */
function prefix_get_products( $request ) {
    // In practice this function would fetch the desired data. Here we are just making stuff up.
    $products = array(
        '1' => 'I am product 1',
        '2' => 'I am product 2',
        '3' => 'I am product 3',
    );

    return rest_ensure_response( $products );
}

/**
 * This is our callback function to return a single product.
 *
 * @param WP_REST_Request $request This function accepts a rest request to process data.
 */
function prefix_get_product( $request ) {
    // In practice this function would fetch the desired data. Here we are just making stuff up.
    $products = array(
        '1' => 'I am product 1',
        '2' => 'I am product 2',
        '3' => 'I am product 3',
    );

    // Here we are grabbing the 'id' path variable from the $request object. WP_REST_Request implements ArrayAccess, which allows us to grab properties as though it is an array.
    $id = (string) $request['id'];

    if ( isset( $products[ $id ] ) ) {
        // Grab the product.
        $product = $products[ $id ];

        // Return the product as a response.
        return rest_ensure_response( $product );
    } else {
        // Return a WP_Error because the request product was not found. In this case we return a 404 because the main resource was not found.
        return new WP_Error( 'rest_product_invalid', esc_html__( 'The product does not exist.', 'my-text-domain' ), array( 'status' => 404 ) );
    }

    // If the code somehow executes to here something bad happened return a 500.
    return new WP_Error( 'rest_api_sad', esc_html__( 'Something went horribly wrong.', 'my-text-domain' ), array( 'status' => 500 ) );
}

/**
 * This function is where we register our routes for our example endpoint.
 */
function prefix_register_product_routes() {
    // Here we are registering our route for a collection of products.
    register_rest_route( 'my-shop/v1', '/products', array(
        // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
        'methods'  => WP_REST_Server::READABLE,
        // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
        'callback' => 'prefix_get_products',
    ) );
    // Here we are registering our route for single products. The (?P<id>[\d]+) is our path variable for the ID, which, in this example, can only be some form of positive number.
    register_rest_route( 'my-shop/v1', '/products/(?P<id>[\d]+)', array(
        // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
        'methods'  => WP_REST_Server::READABLE,
        // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
        'callback' => 'prefix_get_product',
    ) );
}

add_action( 'rest_api_init', 'prefix_register_product_routes' );
```

The above example covers a lot. The important part to note is that in the second route we register, we add on a path variable `/(?P[\d]+)` to our resource path `/products`. The path variable is a regular expression. In this case it uses `[\d]+` to signify that should be any numerical character at least once. If you are using numeric IDs for your resources, then this is a great example of how to use a path variable. When using path variables, we now have to be careful around what can be matched as it is user input.

The regex luckily will filter out anything that is not numerical. However, what if the product for the requested ID doesn’t exist. We need to do error handling. You can see the basic way we are handling errors in the code example above. When you return a `WP_Error` in your endpoint callbacks the API server will automatically handle serving the error to the client.

Although this section is about routes, we have covered quite a bit about endpoints. Endpoints and routes are interrelated, but they definitely have distinctions.

## Endpoints

Endpoints are the destination that a route needs to map to. For any given route, you could have a number of different endpoints registered to it. We will expand on our fictitious eCommerce plugin, to better show the distinction between routes and endpoints. We are going to create two endpoints that exist at the `/wp-json/my-shop/v1/products/` route. One endpoint uses the HTTP verb `GET` to get products, and the other endpoint uses the HTTP verb `POST` to create a new product.

```
/**
 * This is our callback function to return our products.
 *
 * @param WP_REST_Request $request This function accepts a rest request to process data.
 */
function prefix_get_products( $request ) {
    // In practice this function would fetch the desired data. Here we are just making stuff up.
    $products = array(
        '1' => 'I am product 1',
        '2' => 'I am product 2',
        '3' => 'I am product 3',
    );

    return rest_ensure_response( $products );
}

/**
 * This is our callback function to return a single product.
 *
 * @param WP_REST_Request $request This function accepts a rest request to process data.
 */
function prefix_create_product( $request ) {
    // In practice this function would create a product. Here we are just making stuff up.
   return rest_ensure_response( 'Product has been created' );
}

/**
 * This function is where we register our routes for our example endpoint.
 */
function prefix_register_product_routes() {
    // Here we are registering our route for a collection of products and creation of products.
    register_rest_route( 'my-shop/v1', '/products', array(
        array(
            // By using this constant we ensure that when the WP_REST_Server changes, our readable endpoints will work as intended.
            'methods'  => WP_REST_Server::READABLE,
            // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
            'callback' => 'prefix_get_products',
        ),
        array(
            // By using this constant we ensure that when the WP_REST_Server changes, our create endpoints will work as intended.
            'methods'  => WP_REST_Server::CREATABLE,
            // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
            'callback' => 'prefix_create_product',
        ),
    ) );
}

add_action( 'rest_api_init', 'prefix_register_product_routes' );
```

Depending on what HTTP Method we use for the route `/wp-json/my-shop/v1/products`, we are matched to a different endpoint and a different callback is fired. When we use `POST` we trigger the `prefix_create_product()` callback, and when we use `GET` we trigger the `prefix_get_products()` callback.

There are a number of different HTTP methods and the REST API can make use of any of them.

### HTTP 方法

HTTP 方法有时被称为 HTTP 动词。它们只是通过 HTTP 进行通信的不同方式。WordPress REST API 主要使用以下几种：

- `GET` 应用于从 API 检索数据。
- `POST` 应用于创建新资源（例如用户、文章、分类法）。
- `PUT` 应用于更新资源。
- `DELETE` 应用于删除资源。
- `OPTIONS` 应用于提供关于资源的上下文信息。

需要注意的是，并非所有客户端都支持这些方法，因为它们是在 HTTP 1.1 中引入的。幸运的是，API 为这些情况提供了变通方案。如果你想删除一个资源但无法发送 `DELETE` 请求，那么你可以在请求中使用 `_method` 参数或 `X-HTTP-Method-Override` 请求头。其工作原理是：你将发送一个 `POST` 请求到 `https://ourawesomesite.com/wp-json/my-shop/v1/products/1?_method=DELETE`。这样，即使你的客户端无法在请求中发送正确的 HTTP 方法，或者存在防火墙阻止了 DELETE 请求，你也能成功删除 1 号产品。

HTTP 方法与路由和回调函数相结合，构成了端点的核心。

### 回调函数

REST API 目前仅支持两种端点回调类型：`callback` 和 `permissions_callback`。主回调函数负责处理与资源的交互，权限回调函数则管理用户对端点的访问权限。您可以在注册端点时添加额外信息来扩展回调功能，随后通过挂载 `rest_pre_dispatch`、`rest_dispatch_request` 或 `rest_post_dispatch` 钩子来触发自定义回调。

#### 端点回调

删除端点的主回调应仅删除资源并在响应中返回其副本。创建端点的主回调应仅创建资源并返回与新创建数据匹配的响应。更新回调应仅修改实际存在的资源。读取回调应仅检索已存在的数据。必须考虑幂等性概念。

在 REST API 中，幂等性意味着如果向端点发送相同请求，服务器将以相同方式处理请求。假设我们的读取端点不具备幂等性，那么每次请求时服务器状态都会被修改，即使我们只是尝试获取数据。这可能是灾难性的——每当有人从服务器获取数据时，内部状态都会发生变化。必须确保读取、更新和删除端点不会产生有害副作用，严格遵循其设计功能。

在 REST API 中，幂等性概念与 HTTP 方法而非端点回调绑定。任何使用 `GET`、`HEAD`、`TRACE`、`OPTIONS`、`PUT` 或 `DELETE` 的回调都不应产生副作用。`POST` 请求不具备幂等性，通常用于创建资源。如果创建幂等的创建方法，则只会生成一个资源，因为重复相同请求不会对服务器产生额外副作用。对于创建操作，重复发送相同请求时服务器应每次都生成新资源。

为限制端点使用，我们需要注册权限回调。

#### Permissions Callback

Permissions callbacks are extremely important for security with the WordPress REST API. If you have any private data that should not be displayed publicly, then you need to have permissions callbacks registered for your endpoints. Below is an example of how to register permissions callbacks.

```
/**
 * This is our callback function that embeds our resource in a WP_REST_Response
 */
function prefix_get_private_data() {
    // rest_ensure_response() wraps the data we want to return into a WP_REST_Response, and ensures it will be properly returned.
    return rest_ensure_response( 'This is private data.' );
}

/**
 * This is our callback function that embeds our resource in a WP_REST_Response
 */
function prefix_get_private_data_permissions_check() {
    // Restrict endpoint to only users who have the edit_posts capability.
    if ( ! current_user_can( 'edit_posts' ) ) {
        return new WP_Error( 'rest_forbidden', esc_html__( 'OMG you can not view private data.', 'my-text-domain' ), array( 'status' => 401 ) );
    }

    // This is a black-listing approach. You could alternatively do this via white-listing, by returning false here and changing the permissions check.
    return true;
}

/**
 * This function is where we register our routes for our example endpoint.
 */
function prefix_register_example_routes() {
    // register_rest_route() handles more arguments but we are going to stick to the basics for now.
    register_rest_route( 'my-plugin/v1', '/private-data', array(
        // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
        'methods'  => WP_REST_Server::READABLE,
        // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
        'callback' => 'prefix_get_private_data',
        // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
        'permissions_callback' => 'prefix_get_private_data_permissions_check',
    ) );
}

add_action( 'rest_api_init', 'prefix_register_example_routes' );
```

If you try out this endpoint without any Authentication enabled then you will also be returned the error response, preventing you from seeing the data. Authentication is a huge topic and eventually a portion of this chapter will be created to show you how to create your own authentication processes.

### Arguments

When making requests to an endpoint you might need to specify extra parameters to change the response. These extra parameters can be added while registering endpoints. Let’s look at an example of how to use arguments with an endpoint.

```
/**
 * This is our callback function that embeds our resource in a WP_REST_Response
 */
function prefix_get_colors( $request ) {
    // In practice this function would fetch the desired data. Here we are just making stuff up.
    $colors = array(
        'blue',
        'blue',
        'red',
        'red',
        'green',
        'green',
    );

    if ( isset( $request['filter'] ) ) {
       $filtered_colors = array();
       foreach ( $colors as $color ) {
           if ( $request['filter'] === $color ) {
               $filtered_colors[] = $color;
           }
       }
       return rest_ensure_response( $filtered_colors );
    }
    return rest_ensure_response( $colors );
}

/**
 * We can use this function to contain our arguments for the example product endpoint.
 */
function prefix_get_color_arguments() {
    $args = array();
    // Here we are registering the schema for the filter argument.
    $args['filter'] = array(
        // description should be a human readable description of the argument.
        'description' => esc_html__( 'The filter parameter is used to filter the collection of colors', 'my-text-domain' ),
        // type specifies the type of data that the argument should be.
        'type'        => 'string',
        // enum specified what values filter can take on.
        'enum'        => array( 'red', 'green', 'blue' ),
    );
    return $args;
}

/**
 * This function is where we register our routes for our example endpoint.
 */
function prefix_register_example_routes() {
    // register_rest_route() handles more arguments but we are going to stick to the basics for now.
    register_rest_route( 'my-colors/v1', '/colors', array(
        // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
        'methods'  => WP_REST_Server::READABLE,
        // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
        'callback' => 'prefix_get_colors',
        // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
        'args' => prefix_get_color_arguments(),
    ) );
}

add_action( 'rest_api_init', 'prefix_register_example_routes' );
```

在此示例中，我们已为 `filter` 参数进行了定义。我们可以在请求端点时将该参数作为查询参数传递。如果向 `https://ourawesomesitem.com/my-colors/v1/colors?filter=blue` 发起 `GET` 请求，将仅返回集合中的蓝色数据。您也可以将这些参数作为请求体参数传递，而非置于查询字符串中。要理解查询参数与请求体参数的区别，请查阅 HTTP 规范说明。查询参数位于 URL 附加的查询字符串中，而请求体参数则直接嵌入 HTTP 请求的正文内。

我们已为端点创建了参数，但如何验证该参数是否为字符串，并判断其是否匹配红、绿或蓝的取值？为此，我们需要为该参数指定验证回调函数。

#### Validation

Validation and sanitization are extremely important for security in the API. The validate callback (in WP 4.6+), fires before the sanitize callback. You should use the `validate_callback` for your arguments to verify whether the input you are receiving is valid. The `sanitize_callback` should be used to transform the argument input or clean out unwanted parts out of the argument, before the argument is processed by the main callback.

In the example above, we need to verify that the `filter` parameter is a string, and it matches the value red, green, or blue. Let’s look at what the code looks like after adding in a `validate_callback`.

```
/**
 * This is our callback function that embeds our resource in a WP_REST_Response
 */
function prefix_get_colors( $request ) {
    // In practice this function would fetch more practical data. Here we are just making stuff up.
    $colors = array(
        'blue',
        'blue',
        'red',
        'red',
        'green',
        'green',
    );

    if ( isset( $request['filter'] ) ) {
       $filtered_colors = array();
       foreach ( $colors as $color ) {
           if ( $request['filter'] === $color ) {
               $filtered_colors[] = $color;
           }
       }
       return rest_ensure_response( $filtered_colors );
    }
    return rest_ensure_response( $colors );
}
/**
 * Validate a request argument based on details registered to the route.
 *
 * @param  mixed            $value   Value of the 'filter' argument.
 * @param  WP_REST_Request  $request The current request object.
 * @param  string           $param   Key of the parameter. In this case it is 'filter'.
 * @return WP_Error|boolean
 */
function prefix_filter_arg_validate_callback( $value, $request, $param ) {
    // If the 'filter' argument is not a string return an error.
    if ( ! is_string( $value ) ) {
        return new WP_Error( 'rest_invalid_param', esc_html__( 'The filter argument must be a string.', 'my-text-domain' ), array( 'status' => 400 ) );
    }

    // Get the registered attributes for this endpoint request.
    $attributes = $request->get_attributes();

    // Grab the filter param schema.
    $args = $attributes['args'][ $param ];

    // If the filter param is not a value in our enum then we should return an error as well.
    if ( ! in_array( $value, $args['enum'], true ) ) {
        return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not one of %s' ), $param, implode( ', ', $args['enum'] ) ), array( 'status' => 400 ) );
    }
}

/**
 * We can use this function to contain our arguments for the example product endpoint.
 */
function prefix_get_color_arguments() {
    $args = array();
    // Here we are registering the schema for the filter argument.
    $args['filter'] = array(
        // description should be a human readable description of the argument.
        'description' => esc_html__( 'The filter parameter is used to filter the collection of colors', 'my-text-domain' ),
        // type specifies the type of data that the argument should be.
        'type'        => 'string',
        // enum specified what values filter can take on.
        'enum'        => array( 'red', 'green', 'blue' ),
        // Here we register the validation callback for the filter argument.
        'validate_callback' => 'prefix_filter_arg_validate_callback',
    );
    return $args;
}

/**
 * This function is where we register our routes for our example endpoint.
 */
function prefix_register_example_routes() {
    // register_rest_route() handles more arguments but we are going to stick to the basics for now.
    register_rest_route( 'my-colors/v1', '/colors', array(
        // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
        'methods'  => WP_REST_Server::READABLE,
        // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
        'callback' => 'prefix_get_colors',
        // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
        'args' => prefix_get_color_arguments(),
    ) );
}

add_action( 'rest_api_init', 'prefix_register_example_routes' );
```

#### Sanitizing

In the above example, we do not need to use a `sanitize_callback`, because we are restricting input to only values in our enum. If we did not have strict validation and accepted any string as a parameter, we would definitely need to register a `sanitize_callback`. What if we wanted to update a content field and the user entered something like `alert('ZOMG Hacking you');`. The field value could potentially be a executable script. To strip out unwanted data or to transform data into a desired format we need to register a `sanitize_callback` for our arguments. Here is an example of how to use WordPress’s `sanitize_text_field()` for a sanitize callback:

```
/**
 * This is our callback function that embeds our resource in a WP_REST_Response.
 *
 * The parameter is already sanitized by this point so we can use it without any worries.
 */
function prefix_get_item( $request ) {
    if ( isset( $request['data'] ) ) {
        return rest_ensure_response( $request['data'] );
    }

    return new WP_Error( 'rest_invalid', esc_html__( 'The data parameter is required.', 'my-text-domain' ), array( 'status' => 400 ) );
}

/**
 * Validate a request argument based on details registered to the route.
 *
 * @param  mixed            $value   Value of the 'filter' argument.
 * @param  WP_REST_Request  $request The current request object.
 * @param  string           $param   Key of the parameter. In this case it is 'filter'.
 * @return WP_Error|boolean
 */
function prefix_data_arg_validate_callback( $value, $request, $param ) {
    // If the 'data' argument is not a string return an error.
    if ( ! is_string( $value ) ) {
        return new WP_Error( 'rest_invalid_param', esc_html__( 'The filter argument must be a string.', 'my-text-domain' ), array( 'status' => 400 ) );
    }
}

/**
 * Sanitize a request argument based on details registered to the route.
 *
 * @param  mixed            $value   Value of the 'filter' argument.
 * @param  WP_REST_Request  $request The current request object.
 * @param  string           $param   Key of the parameter. In this case it is 'filter'.
 * @return WP_Error|boolean
 */
function prefix_data_arg_sanitize_callback( $value, $request, $param ) {
    // It is as simple as returning the sanitized value.
    return sanitize_text_field( $value );
}

/**
 * We can use this function to contain our arguments for the example product endpoint.
 */
function prefix_get_data_arguments() {
    $args = array();
    // Here we are registering the schema for the filter argument.
    $args['data'] = array(
        // description should be a human readable description of the argument.
        'description' => esc_html__( 'The data parameter is used to be sanitized and returned in the response.', 'my-text-domain' ),
        // type specifies the type of data that the argument should be.
        'type'        => 'string',
        // Set the argument to be required for the endpoint.
        'required'    => true,
        // We are registering a basic validation callback for the data argument.
        'validate_callback' => 'prefix_data_arg_validate_callback',
        // Here we register the validation callback for the filter argument.
        'sanitize_callback' => 'prefix_data_arg_sanitize_callback',
    );
    return $args;
}

/**
 * This function is where we register our routes for our example endpoint.
 */
function prefix_register_example_routes() {
    // register_rest_route() handles more arguments but we are going to stick to the basics for now.
    register_rest_route( 'my-plugin/v1', '/sanitized-data', array(
        // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
        'methods'  => WP_REST_Server::READABLE,
        // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
        'callback' => 'prefix_get_item',
        // Here we register our permissions callback. The callback is fired before the main callback to check if the current user can access the endpoint.
        'args' => prefix_get_data_arguments(),
    ) );
}

add_action( 'rest_api_init', 'prefix_register_example_routes' );
```

## 总结

我们已经介绍了为 WordPress REST API 注册端点的基本知识。路由是端点所在的 URI。端点是一组回调函数、方法、参数和其他选项的集合。使用 `register_rest_route()` 时，每个端点都会映射到一个路由。默认情况下，一个端点可以支持多种 HTTP 方法、一个主回调函数、一个权限回调函数以及已注册的参数。我们可以注册端点来满足与 WordPress 交互的任何用例。端点是 REST API 的核心交互点，但要充分利用这个强大的 API，还有许多其他主题需要探索和理解。

# 模式

## 概述

模式是一种数据，它告诉我们其他数据应如何组织。大多数数据库都实现了某种形式的模式，使我们能够以更结构化的方式处理数据。WordPress REST API 使用 JSON Schema 来处理其数据的结构。您可以在不使用模式的情况下实现端点，但您将错过许多功能。这取决于您决定什么最适合您。

## JSON 模式

首先，我们来简单谈谈 JSON。JSON 是一种人类可读的数据格式，类似于 JavaScript 对象。JSON 代表 JavaScript 对象表示法。JSON 正迅速普及，似乎正席卷数据结构领域。WordPress REST API 使用一种称为 JSON 模式的特殊规范来处理 JSON。要了解更多关于 JSON 模式的信息，请查看 [JSON 模式网站](http://json-schema.org/) 和这篇 [更易于理解的 JSON 模式介绍](https://spacetelescope.github.io/understanding-json-schema/index.html)。模式为我们带来了许多好处：改进的测试、可发现性以及整体更好的结构。让我们看一个 JSON 数据块。

```
{
    "shouldBeArray": 'LOL definitely not an array',
    "shouldBeInteger": ['lolz', 'you', 'need', 'schema'],
    "shouldBeString": 123456789
}
```

JSON 解析器会毫无问题地处理这些数据，并且不会抱怨任何问题，因为它是有效的 JSON。客户端和服务器对数据一无所知，也不知道预期什么，它们只看到 JSON。通过实现模式，我们实际上可以简化代码库。模式将有助于更好地构建我们的数据，使我们的应用程序能更轻松地理解与 WordPress REST API 的交互。WordPress REST API 并不强制您使用模式，但鼓励使用。有两种方式将模式数据整合到 API 中：资源的模式和已注册参数的模式。

## Resource Schema

The schema for a resource indicates what fields are present for a particular object. When we register our routes we can also specify the resource schema for the route. Let’s look at what a simple comment schema might look like in a PHP representation of JSON schema.  

```
// Register our routes.
function prefix_register_my_comment_route() {
	register_rest_route( 'my-namespace/v1', '/comments', array(
		// Notice how we are registering multiple endpoints the 'schema' equates to an OPTIONS request.
		array(
			'methods'  => 'GET',
			'callback' => 'prefix_get_comment_sample',
		),
		// Register our schema callback.
		'schema' => 'prefix_get_comment_schema',
	) );
}

add_action( 'rest_api_init', 'prefix_register_my_comment_route' );

/**
 * Grabs the five most recent comments and outputs them as a rest response.
 *
 * @param WP_REST_Request $request Current request.
 */
function prefix_get_comment_sample( $request ) {
	$args = array(
		'post_per_page' => 5,
	);
	$comments = get_comments( $args );

	$data = array();

	if ( empty( $comments ) ) {
		return rest_ensure_response( $data );
	}

	foreach ( $comments as $comment ) {
		$response = prefix_rest_prepare_comment( $comment, $request );
		$data[] = prefix_prepare_for_collection( $response );
	}

	// Return all of our comment response data.
	return rest_ensure_response( $data );
}

/**
 * Matches the comment data to the schema we want.
 *
 * @param WP_Comment $comment The comment object whose response is being prepared.
 */
function prefix_rest_prepare_comment( $comment, $request ) {
	$comment_data = array();

	$schema = prefix_get_comment_schema( $request );

	// We are also renaming the fields to more understandable names.
	if ( isset( $schema['properties']['id'] ) ) {
		$comment_data['id'] = (int) $comment->comment_id;
	}

	if ( isset( $schema['properties']['author'] ) ) {
		$comment_data['author'] = (int) $comment->user_id;
	}

	if ( isset( $schema['properties']['content'] ) ) {
		$comment_data['content'] = apply_filters( 'comment_text', $comment->comment_content, $comment );
	}

	return rest_ensure_response( $comment_data );
}

/**
 * Prepare a response for inserting into a collection of responses.
 *
 * This is copied from WP_REST_Controller class in the WP REST API v2 plugin.
 *
 * @param WP_REST_Response $response Response object.
 * @return array Response data, ready for insertion into collection data.
 */
function prefix_prepare_for_collection( $response ) {
	if ( ! ( $response instanceof WP_REST_Response ) ) {
		return $response;
	}

	$data = (array) $response->get_data();
	$server = rest_get_server();

	if ( method_exists( $server, 'get_compact_response_links' ) ) {
		$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
	} else {
		$links = call_user_func( array( $server, 'get_response_links' ), $response );
	}

	if ( ! empty( $links ) ) {
		$data['_links'] = $links;
	}

	return $data;
}

/**
 * Get our sample schema for comments.
 *
 * @param WP_REST_Request $request Current request.
 */
function prefix_get_comment_schema( $request ) {
	$schema = array(
		// This tells the spec of JSON Schema we are using which is draft 4.
		'$schema'              => 'http://json-schema.org/draft-04/schema#',
		// The title property marks the identity of the resource.
		'title'                => 'comment',
		'type'                 => 'object',
		// In JSON Schema you can specify object properties in the properties attribute.
		'properties'           => array(
			'id' => array(
				'description'  => esc_html__( 'Unique identifier for the object.', 'my-textdomain' ),
				'type'         => 'integer',
				'context'      => array( 'view', 'edit', 'embed' ),
				'readonly'     => true,
			),
			'author' => array(
				'description'  => esc_html__( 'The id of the user object, if author was a user.', 'my-textdomain' ),
				'type'         => 'integer',
			),
			'content' => array(
				'description'  => esc_html__( 'The content for the object.', 'my-textdomain' ),
				'type'         => 'string',
			),
		),
	);

	return $schema;
}
```

If you notice, each comment resource now matches up to our schema that we specified. We made this switch in `prefix_rest_prepare_comment()`. By creating schema for our resources, we can now view this schema by making `OPTIONS` requests. Why is this useful? If we wanted other languages, JavaScript for example, to interpret our data and validate the data from our endpoint, JavaScript would need to know how our data is structured. When we provide schema, we open the doors for other authors, and ourselves, to build on top of our endpoints in a consistent manner.

Schema provides machine readable data, so potentially anything that can read JSON can understand what kind of data it is looking at. When we look at the API index by making a `GET` request to `https://ourawesomesite.com/wp-json/`, we are returned the schema of our API, enabling others to write client libraries to interpret our data. This process of reading schema data is known as discovery. When we have provided schema for a resource we make that resource discoverable via `OPTIONS` requests to that route. Exposing resource schema is only one part of our schema puzzle. We also want to use schema for our registered arguments.

## Argument Schema

When we register request arguments for an endpoint, we can also use JSON Schema to provide us data about what the arguments should be. This enables us to write validation libraries that can be reused as our endpoints expand. Schema is more work upfront, but if you are going to write a production application that will grow, you should definitely consider using schema. Let’s look at an example of using argument schema and validation.

```
// Register our routes.
function prefix_register_my_arg_route() {
	register_rest_route( 'my-namespace/v1', '/schema-arg', array(
		// Here we register our endpoint.
		array(
			'methods'  => 'GET',
			'callback' => 'prefix_get_item',
			'args' => prefix_get_endpoint_args(),
		),
	) );
}

// Hook registration into 'rest_api_init' hook.
add_action( 'rest_api_init', 'prefix_register_my_arg_route' );

/**
 * Returns the request argument `my-arg` as a rest response.
 *
 * @param WP_REST_Request $request Current request.
 */
function prefix_get_item( $request ) {
	// If we didn't use required in the schema this would throw an error when my arg is not set.
	return rest_ensure_response( $request['my-arg'] );
}

/**
 * Get the argument schema for this example endpoint.
 */
function prefix_get_endpoint_args() {
	$args = array();

	// Here we add our PHP representation of JSON Schema.
	$args['my-arg'] = array(
		'description'       => esc_html__( 'This is the argument our endpoint returns.', 'my-textdomain' ),
		'type'              => 'string',
		'validate_callback' => 'prefix_validate_my_arg',
		'sanitize_callback' => 'prefix_sanitize_my_arg',
		'required'          => true,
	);

	return $args;
}

/**
 * Our validation callback for `my-arg` parameter.
 *
 * @param mixed           $value   Value of the my-arg parameter.
 * @param WP_REST_Request $request Current request object.
 * @param string          $param   The name of the parameter in this case, 'my-arg'.
 */
function prefix_validate_my_arg( $value, $request, $param ) {
	$attributes = $request->get_attributes();

	if ( isset( $attributes['args'][ $param ] ) ) {
		$argument = $attributes['args'][ $param ];
		// Check to make sure our argument is a string.
		if ( 'string' === $argument['type'] && ! is_string( $value ) ) {
			return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%1$s is not of type %2$s', 'my-textdomain' ), $param, 'string' ), array( 'status' => 400 ) );
		}
	} else {
		// This code won't execute because we have specified this argument as required.
		// If we reused this validation callback and did not have required args then this would fire.
		return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%s was not registered as a request argument.', 'my-textdomain' ), $param ), array( 'status' => 400 ) );
	}

	// If we got this far then the data is valid.
	return true;
}

/**
 * Our santization callback for `my-arg` parameter.
 *
 * @param mixed           $value   Value of the my-arg parameter.
 * @param WP_REST_Request $request Current request object.
 * @param string          $param   The name of the parameter in this case, 'my-arg'.
 */
function prefix_sanitize_my_arg( $value, $request, $param ) {
	$attributes = $request->get_attributes();

	if ( isset( $attributes['args'][ $param ] ) ) {
		$argument = $attributes['args'][ $param ];
		// Check to make sure our argument is a string.
		if ( 'string' === $argument['type'] ) {
			return sanitize_text_field( $value );
		}
	} else {
		// This code won't execute because we have specified this argument as required.
		// If we reused this validation callback and did not have required args then this would fire.
		return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%s was not registered as a request argument.', 'my-textdomain' ), $param ), array( 'status' => 400 ) );
	}

	// If we got this far then something went wrong don't use user input.
	return new WP_Error( 'rest_api_sad', esc_html__( 'Something went terribly wrong.', 'my-textdomain' ), array( 'status' => 500 ) );
}
```


In the example above we have abstracted away from using the `'my-arg'` name. We can use these validation and sanitizing functions for any other argument that should be a string we have specified schema for. As your codebase and endpoints grow, schema will help keep your code lightweight and maintainable. Without schema you can validate and sanitize, however it will be more difficult to keep track of which functions should be validating what. By adding schema to request arguments we can also expose our argument schema to clients, so validation libraries can be built client side which can help performance by preventing invalid requests from ever being sent to the API.

[info]If you are uncomfortable with using schema, it is still possible to have validate/sanitize callbacks for each of your arguments, and in some cases it will make the most sense to do a custom validation.[/info]

## 概述

模式在某些时候可能显得多余，甚至像是额外的工作，但如果你想要可维护、可发现且易于扩展的端点，使用模式是必不可少的。模式还能帮助为人类和计算机自动记录你的端点！

# 插件安全

此内容已迁移至通用 API 手册中的[安全页面](https://developer.wordpress.org/apis/security/)。

但仍有更多内容可供阅读...

# 检查用户权限

如果你的插件允许用户提交数据——无论是在管理端还是公共端——都应该检查用户权限。

## 用户角色与权限

创建高效安全层的最重要步骤是建立用户权限系统。WordPress 通过[用户角色与权限](https://developer.wordpress.org/plugins/users/roles-and-capabilities/)的形式提供了这一功能。

每个登录 WordPress 的用户都会根据其用户角色自动分配特定的用户权限。

**用户角色**只是表示用户所属组的另一种说法。每个组都有一组特定的预定义权限。

例如，你网站的主要用户将拥有管理员角色，而其他用户可能拥有编辑或作者等角色。一个角色可以分配给多个用户，即一个网站可能有两个管理员。

**用户权限**是你分配给每个用户或用户角色的具体许可。

例如，管理员拥有 `manage_options` 权限，这允许他们查看、编辑和保存网站选项。而编辑则缺乏此权限，这将阻止他们操作选项。

这些权限会在管理界面的各个节点进行检查。根据分配给角色的权限，WordPress 体验中的菜单、功能和其他方面可能会被添加或移除。

**在构建插件时，请确保仅在当前用户拥有必要权限时才运行你的代码。**

### 层级结构

用户角色越高，拥有的权限就越多。每个用户角色都会继承层级结构中前序角色的所有权限。

例如，在单站点安装中最高级别的用户角色"管理员"，会继承以下角色及其权限："订阅者"、"投稿人"、"作者"和"编辑"。

## Examples

### No Restrictions

The example below creates a link on the frontend which gives the ability to trash posts. Because this code does not check user capabilities, **it allows any visitor to the site to trash posts!**

```
/**
 * Generate a Delete link based on the homepage url.
 *
 * @param string $content   Existing content.
 *
 * @return string|null
 */
function wporg_generate_delete_link( $content ) {
  // Run only for single post page.
  if ( is_single() && in_the_loop() && is_main_query() ) {
    // Add query arguments: action, post.
    $url = add_query_arg(
      [
        'action' => 'wporg_frontend_delete',
        'post'   => get_the_ID(),
      ], home_url()
    );

    return $content . ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Delete Post', 'wporg' ) . '</a>';
  }

  return null;
}

/**
 * Request handler
 */
function wporg_delete_post() {
  if ( isset( $_GET['action'] ) && 'wporg_frontend_delete' === $_GET['action'] ) {

    // Verify we have a post id.
    $post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );

    // Verify there is a post with such a number.
    $post = get_post( (int) $post_id );
    if ( empty( $post ) ) {
      return;
    }

    // Delete the post.
    wp_trash_post( $post_id );

    // Redirect to admin page.
    $redirect = admin_url( 'edit.php' );
    wp_safe_redirect( $redirect );

    // We are done.
    die;
  }
}

/**
 * Add the delete link to the end of the post content.
 */
add_filter( 'the_content', 'wporg_generate_delete_link' );

/**
 * Register our request handler with the init hook.
 */
add_action( 'init', 'wporg_delete_post' );
```

### Restricted to a Specific Capability

The example above allows any visitor to the site to click on the "Delete" link and trash the post. However, we only want Editors and above to be able to click on the "Delete" link.

To accomplish this, we will check that the current user has the capability `edit_others_posts`, which only Editors or above would have:

```
/**
 * Generate a Delete link based on the homepage url.
 *
 * @param string $content   Existing content.
 *
 * @return string|null
 */
function wporg_generate_delete_link( $content ) {
  // Run only for single post page.
  if ( is_single() && in_the_loop() && is_main_query() ) {
    // Add query arguments: action, post.
    $url = add_query_arg(
      [
        'action' => 'wporg_frontend_delete',
        'post'   => get_the_ID(),
      ], home_url()
    );

    return $content . ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Delete Post', 'wporg' ) . '</a>';
  }

  return null;
}

/**
 * Request handler
 */
function wporg_delete_post() {
  if ( isset( $_GET['action'] ) && 'wporg_frontend_delete' === $_GET['action'] ) {

    // Verify we have a post id.
    $post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );

    // Verify there is a post with such a number.
    $post = get_post( (int) $post_id );
    if ( empty( $post ) ) {
      return;
    }

    // Delete the post.
    wp_trash_post( $post_id );

    // Redirect to admin page.
    $redirect = admin_url( 'edit.php' );
    wp_safe_redirect( $redirect );

    // We are done.
    die;
  }
}

/**
 * Add delete post ability
 */
add_action('plugins_loaded', 'wporg_add_delete_post_ability');

function wporg_add_delete_post_ability() {    
  if ( current_user_can( 'edit_others_posts' ) ) {
    /**
     * Add the delete link to the end of the post content.
     */
    add_filter( 'the_content', 'wporg_generate_delete_link' );

    /**
     * Register our request handler with the init hook.
     */
    add_action( 'init', 'wporg_delete_post' );
  }
}
```

# 数据验证

此内容已迁移至通用 API 手册中的[数据验证页面](https://developer.wordpress.org/apis/security/data-validation/)。

# Nonces

此内容已移至 Common APIs 手册中的 [Nonces 页面](https://developer.wordpress.org/apis/security/nonces/)。

# 保护（清理）输入

此内容已移至 Common APIs 手册中的[数据清理](https://developer.wordpress.org/apis/security/sanitizing/)页面。

# 保护（转义）输出

此内容已移至 Common APIs 手册中的[转义数据](https://developer.wordpress.org/apis/security/escaping/)页面。

# 设置

WordPress 提供了两个核心 API，使管理界面的构建变得简单、安全，并与 WordPress 管理后台的设计保持一致。

[设置 API](https://developer.wordpress.org/plugins/settings/settings-api/) 专注于为开发者提供创建表单和管理表单数据的方法。

[选项 API](https://developer.wordpress.org/plugins/settings/options-api/) 专注于使用简单的键/值系统来管理数据。

## 快速参考

查看使用设置 API 和选项 API [构建自定义设置页面](https://developer.wordpress.org/plugins/settings/custom-settings-page/)的完整示例。

# 自定义设置页面

创建自定义设置页面需要结合以下内容：[创建管理菜单](https://developer.wordpress.org/plugins/administration-menus/)、[使用设置 API](https://developer.wordpress.org/plugins/settings/using-settings-api/) 和 [选项 API](https://developer.wordpress.org/plugins/settings/options-api/)。

[alert]在尝试创建自己的设置页面之前，请先阅读这些章节。[/alert]

下面的示例可通过注释快速参考这些主题。

## Complete Example

Complete example which adds a Top-Level Menu named `WPOrg`, registers a custom option named `wporg_options` and performs the CRUD (create, read, update, delete) logic using Settings API and Options API (including showing error/update messages).

```
/**
 * @internal never define functions inside callbacks.
 * these functions could be run multiple times; this would result in a fatal error.
 */

/**
 * custom option and settings
 */
function wporg_settings_init() {
  // Register a new setting for "wporg" page.
  register_setting( 'wporg', 'wporg_options' );

  // Register a new section in the "wporg" page.
  add_settings_section(
    'wporg_section_developers',
    __( 'The Matrix has you.', 'wporg' ), 'wporg_section_developers_callback',
    'wporg'
  );

  // Register a new field in the "wporg_section_developers" section, inside the "wporg" page.
  add_settings_field(
    'wporg_field_pill', // As of WP 4.6 this value is used only internally.
                        // Use $args' label_for to populate the id inside the callback.
    __( 'Pill', 'wporg' ),
    'wporg_field_pill_cb',
    'wporg',
    'wporg_section_developers',
    array(
      'label_for'         => 'wporg_field_pill',
      'class'             => 'wporg_row',
      'wporg_custom_data' => 'custom',
    )
  );
}

/**
 * Register our wporg_settings_init to the admin_init action hook.
 */
add_action( 'admin_init', 'wporg_settings_init' );

/**
 * Custom option and settings:
 *  - callback functions
 */

/**
 * Developers section callback function.
 *
 * @param array $args  The settings array, defining title, id, callback.
 */
function wporg_section_developers_callback( $args ) {
?>
  <p id="<?php echo esc_attr( $args['id'] ); ?>"><?php esc_html_e( 'Follow the white rabbit.', 'wporg' ); ?></p>
<?php
}

/**
 * Pill field callbakc function.
 *
 * WordPress has magic interaction with the following keys: label_for, class.
 * - the "label_for" key value is used for the "for" attribute of the <label>.
 * - the "class" key value is used for the "class" attribute of the <tr> containing the field.
 * Note: you can add custom key value pairs to be used inside your callbacks.
 *
 * @param array $args
 */
function wporg_field_pill_cb( $args ) {
  // Get the value of the setting we've registered with register_setting()
  $options = get_option( 'wporg_options' );
  ?>
  <select
    id="<?php echo esc_attr( $args['label_for'] ); ?>"
    data-custom="<?php echo esc_attr( $args['wporg_custom_data'] ); ?>"
    name="wporg_options[<?php echo esc_attr( $args['label_for'] ); ?>]">
    <option value="red" <?php echo isset( $options[ $args['label_for'] ] ) ? ( selected( $options[ $args['label_for'] ], 'red', false ) ) : ( '' ); ?>>
      <?php esc_html_e( 'red pill', 'wporg' ); ?>
    </option>
    <option value="blue" <?php echo isset( $options[ $args['label_for'] ] ) ? ( selected( $options[ $args['label_for'] ], 'blue', false ) ) : ( '' ); ?>>
      <?php esc_html_e( 'blue pill', 'wporg' ); ?>
    </option>
  </select>
  <p class="description">
    <?php esc_html_e( 'You take the blue pill and the story ends. You wake in your bed and you believe whatever you want to believe.', 'wporg' ); ?>
  </p>
  <p class="description">
    <?php esc_html_e( 'You take the red pill and you stay in Wonderland and I show you how deep the rabbit-hole goes.', 'wporg' ); ?>
  </p>
  <?php
}

/**
 * Add the top level menu page.
 */
function wporg_options_page() {
  add_menu_page(
    'WPOrg',
    'WPOrg Options',
    'manage_options',
    'wporg',
    'wporg_options_page_html'
  );
}

/**
 * Register our wporg_options_page to the admin_menu action hook.
 */
add_action( 'admin_menu', 'wporg_options_page' );

/**
 * Top level menu callback function
 */
function wporg_options_page_html() {
  // check user capabilities
  if ( ! current_user_can( 'manage_options' ) ) {
    return;
  }

  // add error/update messages

  // check if the user have submitted the settings
  // WordPress will add the "settings-updated" $_GET parameter to the url
  if ( isset( $_GET['settings-updated'] ) ) {
    // add settings saved message with the class of "updated"
    add_settings_error( 'wporg_messages', 'wporg_message', __( 'Settings Saved', 'wporg' ), 'updated' );
  }

// show error/update messages
  settings_errors( 'wporg_messages' );
  ?>
  <div class="wrap">
    <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
    <form action="options.php" method="post">
      <?php
      // output security fields for the registered setting "wporg"
      settings_fields( 'wporg' );
      // output setting sections and their fields
      // (sections are registered for "wporg", each field is registered to a specific section)
      do_settings_sections( 'wporg' );
      // output save settings button
      submit_button( 'Save Settings' );
      ?>
    </form>
  </div>
  <?php
}
```

# Options API

Options API 自 WordPress 1.0 版本引入，允许对 WordPress 选项进行创建、读取、更新和删除操作。结合 [Settings API](https://developer.wordpress.org/plugins/settings/settings-api/) 使用，可以控制设置在页面中定义的选项。

## 选项存储位置？

选项存储在 `{$wpdb->prefix}_options` 表中。`$wpdb->prefix` 由 `wp-config.php` 文件中设置的 `$table_prefix` 变量定义。

## 选项如何存储？

选项在 WordPress 数据库中可以两种方式之一存储：作为单个值或作为值数组。

### 单个值

当保存为单个值时，选项名称对应单个值。

```
// 添加新选项
add_option('wporg_custom_option', 'hello world!');
// 获取选项
$option = get_option('wporg_custom_option');
```

### 值数组

当保存为值数组时，选项名称对应一个数组，该数组本身可能包含键/值对。

```
// 选项数组
$data_r = array('title' => 'hello world!', 1, false );
// 添加新选项
add_option('wporg_custom_option', $data_r);
// 获取选项
$options_r = get_option('wporg_custom_option');
// 输出标题
echo esc_html($options_r['title']);
```

如果您需要处理大量相关选项，将它们存储为数组可以对整体性能产生积极影响。

[info]以独立选项形式访问数据可能导致多次独立的数据库事务，通常数据库事务是开销较大的操作（在时间和服务器资源方面）。当您存储或检索选项数组时，它会在单次事务中完成，这是理想的方式。[/info]

## 函数参考

### 添加选项

- [`add_option()`](https://developer.wordpress.org/reference/functions/add_option/)
- [`add_site_option()`](https://developer.wordpress.org/reference/functions/add_site_option/)

### 获取选项

- [`get_option()`](https://developer.wordpress.org/reference/functions/get_option/)
- [`get_site_option()`](https://developer.wordpress.org/reference/functions/get_site_option/)

### 更新选项

- [`update_option()`](https://developer.wordpress.org/reference/functions/update_option/)
- [`update_site_option()`](https://developer.wordpress.org/reference/functions/update_site_option/)

### 删除选项

- [`delete_option()`](https://developer.wordpress.org/reference/functions/delete_option/)
- [`delete_site_option()`](https://developer.wordpress.org/reference/functions/delete_site_option/)

# Settings API

Settings API 于 WordPress 2.7 版本引入，允许以半自动方式管理包含设置表单的管理页面。您可以通过它定义设置页面、页面内的区块以及区块内的字段。

您可以注册新的设置页面及其内部的区块和字段。也可以通过向现有设置页面注册新的设置区块或字段来扩展其功能。

开发者仍需投入精力组织字段的注册与验证工作，但能避免大量底层选项管理的复杂调试。

[alert]使用 Settings API 时，表单将提交至 `wp-admin/options.php` 并执行严格的权限检查。用户需具备 `manage_options` 权限（在多站点环境下必须是超级管理员）才能提交表单。[/alert]

## 为何使用设置 API？

开发者*可以*忽略此 API 并自行编写设置页面。这就引出了一个问题：此 API 能带来什么好处？以下是部分优势的快速概述。

### 视觉一致性

使用 API 生成界面元素可确保您的设置页面与其余管理内容风格统一。您的界面将遵循相同的样式指南，完美融入系统，并且得益于 WordPress 设计师团队的卓越才华，视觉效果将非常出色！

### 健壮性（面向未来！）

由于该 API 是 WordPress 核心的一部分，任何更新都会自动考虑你的插件设置页面。如果你不使用设置 API 而自行创建界面，WordPress 核心更新更有可能破坏你的自定义功能。同时，有更广泛的用户群体在测试和维护该 API 代码，因此它往往更加稳定。

### 省时省力！

最直接的好处当然是 WordPress API 在幕后为你完成了大量工作。除了提供美观集成的设计外，设置 API 还具备以下功能：

- **处理表单提交**：让 WordPress 自动处理 `$_POST` 数据的获取与存储
- **内置安全措施**：免费获得随机数等额外安全防护机制
- **数据净化处理**：可使用与 WordPress 其他模块相同的字符串安全校验方法

## 函数参考

### 设置注册/注销

- [`register_setting()`](https://developer.wordpress.org/reference/functions/register_setting/)
- [`unregister_setting()`](https://developer.wordpress.org/reference/functions/unregister_setting/)

### 添加字段/区块

- [`add_settings_section()`](https://developer.wordpress.org/reference/functions/add_settings_section/)
- [`add_settings_field()`](https://developer.wordpress.org/reference/functions/add_settings_field/)

### Options Form Rendering

- [`settings_fields()`](https://developer.wordpress.org/reference/functions/settings_fields/)
- [`do_settings_sections()`](https://developer.wordpress.org/reference/functions/do_settings_sections/)
- [`do_settings_fields()`](https://developer.wordpress.org/reference/functions/do_settings_fields/)

### Errors

- [`add_settings_error()`](https://developer.wordpress.org/reference/functions/add_settings_error/)
- [`get_settings_errors()`](https://developer.wordpress.org/reference/functions/get_settings_errors/)
- [`settings_errors()`](https://developer.wordpress.org/reference/functions/settings_errors/)

# 使用设置 API

## 添加设置

您必须使用 [`register_setting()`](https://developer.wordpress.org/reference/functions/register_setting/) 定义新设置，它将在 `{$wpdb->prefix}_options` 表中创建一个条目。

您可以使用 [`add_settings_section()`](https://developer.wordpress.org/reference/functions/add_settings_section/) 在现有页面上添加新部分。

您可以使用 [`add_settings_field()`](https://developer.wordpress.org/reference/functions/add_settings_field/) 向现有部分添加新字段。

[alert][`register_setting()`](https://developer.wordpress.org/reference/functions/register_setting/) 以及提到的 `add_settings_*()` 函数都应添加到 `admin_init` 操作钩子中。[/alert]

### 添加设置项

```
register_setting(
  string $option_group,
  string $option_name,
  callable $sanitize_callback = ''
);
```

关于所用参数的完整说明，请参阅 [`register_setting()`](https://developer.wordpress.org/reference/functions/register_setting/) 函数参考文档。

### 添加设置区块

```
add_settings_section(
  string $id,
  string $title,
  callable $callback,
  string $page
);
```

区块是您在 WordPress 设置页面上看到的、具有共享标题的设置组。在您的插件中，您可以向现有设置页面添加新区块，而无需创建全新的页面。这使您的插件更易于维护，并为用户减少了需要学习的新页面数量。

有关所用参数的完整说明，请参阅关于 [`add_settings_section()`](https://developer.wordpress.org/reference/functions/add_settings_section/) 的函数参考。

### 添加字段

```
add_settings_field(
  string $id,
  string $title,
  callable $callback,
  string $page,
  string $section = 'default',
  array $args = []
);
```

关于所用参数的完整说明，请参阅 [`add_settings_field()`](https://developer.wordpress.org/reference/functions/add_settings_field/) 的函数参考。

### Example

```
function wporg_settings_init() {
  // register a new setting for "reading" page
  register_setting('reading', 'wporg_setting_name');

  // register a new section in the "reading" page
  add_settings_section(
    'wporg_settings_section',
    'WPOrg Settings Section', 'wporg_settings_section_callback',
    'reading'
  );

  // register a new field in the "wporg_settings_section" section, inside the "reading" page
  add_settings_field(
    'wporg_settings_field',
    'WPOrg Setting', 'wporg_settings_field_callback',
    'reading',
    'wporg_settings_section'
  );
}

/**
 * register wporg_settings_init to the admin_init action hook
 */
add_action('admin_init', 'wporg_settings_init');

/**
 * callback functions
 */

// section content cb
function wporg_settings_section_callback() {
  echo '<p>WPOrg Section Introduction.</p>';
}

// field content cb
function wporg_settings_field_callback() {
  // get the value of the setting we've registered with register_setting()
  $setting = get_option('wporg_setting_name');
  // output the field
  ?>
  <input type="text" name="wporg_setting_name" value="<?php echo isset( $setting ) ? esc_attr( $setting ) : ''; ?>">
  <?php
}
```

## 获取设置

```
get_option(
  string $option,
  mixed $default = false
);
```

获取设置通过 [`get_option()`](https://developer.wordpress.org/reference/functions/get_option/) 函数完成。

该函数接受两个参数：选项的名称和该选项的可选默认值。

### Example

```
// Get the value of the setting we've registered with register_setting()
$setting = get_option('wporg_setting_name');
```

# 简码

出于安全考虑，WordPress 内容中禁止运行 PHP；为了允许与内容进行动态交互，WordPress 2.5 版本引入了简码。

简码是一种宏，可用于与内容进行动态交互，例如从文章附件中创建相册或渲染视频。

## 为何使用简码？

简码是一种有价值的方式，既能保持内容简洁和语义化，又能让最终用户在一定程度上通过编程方式改变内容的呈现方式。

当最终用户使用简码在文章中添加照片库时，他们使用尽可能少的数据来指示相册应如何呈现。

优点：

- 不会向文章内容添加标记，这意味着标记和样式可以轻松地即时或稍后修改。
- 简码还可以接受参数，允许用户根据每个实例修改简码的行为。

## 内置简码

默认情况下，WordPress 包含以下简码：

- `[caption]` – 允许在内容周围添加标题
- `[gallery]` – 允许显示图片库
- `[audio]` – 允许嵌入和播放音频文件
- `[video]` – 允许嵌入和播放视频文件
- `[playlist]` – 允许显示音频或视频文件集合
- `[embed]` – 允许包装嵌入项目

## 简码最佳实践

开发简码的最佳实践包括[插件开发最佳实践](https://developer.wordpress.org/plugins/plugin-basics/best-practices/)以及以下列表：

- **始终返回！** 简码本质上是过滤器，因此创建“[副作用](https://en.wikipedia.org/wiki/Side_effect_(computer_science))”会导致意外错误。
- 为简码名称添加前缀，以避免与其他插件冲突。
- 清理输入并转义输出。
- 向用户提供所有简码属性的清晰文档。

## 快速参考

查看使用[基本简码结构、处理自闭合和包含场景、简码嵌套以及保护输出](https://developer.wordpress.org/plugins/shortcodes/shortcodes-with-parameters/#complete-example)的完整示例。

## 外部资源

- [WordPress 简码生成器](https://generatewp.com/shortcodes/)

# Basic Shortcodes

## Add a Shortcode

It is possible to add your own shortcodes by using the Shortcode API. The process involves registering a callback `$func` to a shortcode `$tag` using `add_shortcode()`.

```
add_shortcode(
  string $tag,
  callable $func
);
```

`[wporg]` is your new shortcode. The use of the shortcode will trigger the `wporg_shortcode` callback function.

```
add_shortcode( 'wporg', 'wporg_shortcode' );
function wporg_shortcode( $atts = [], $content = null) {
  // do something to $content
  // always return
  return $content;
}
```

## Remove a Shortcode

It is possible to remove shortcodes by using the Shortcode API. The process involves removing a registered `$tag` using [`remove_shortcode()`](https://developer.wordpress.org/reference/functions/remove_shortcode/).

```
remove_shortcode(
  string $tag
);
```

Make sure that the shortcode have been registered before attempting to remove. Specify a higher priority number for [`add_action()`](https://developer.wordpress.org/reference/functions/add_action/) or hook into an action hook that is run later.

## Check if a Shortcode Exists

To check whether a shortcode has been registered use [`shortcode_exists()`](https://developer.wordpress.org/reference/functions/shortcode_exists/).

# Enclosing Shortcodes

The are two scenarios for using shortcodes:

- The shortcode is a self-closing tag like we seen in the [Basic Shortcodes](https://developer.wordpress.org/plugins/shortcodes/basic-shortcodes/) section.
- The shortcode is enclosing content.

## Enclosing Content

Enclosing content with a shortcode allows manipulations on the enclosed content.

```
[wporg]content to manipulate[/wporg]
```

As seen above, all you need to do in order to enclose a section of content is add a beginning `[$tag]` and an end `[/$tag]`, similar to HTML.

## Processing Enclosed Content

Lets get back to our original `[wporg]` shortcode code:

```
function wporg_shortcode( $atts = array(), $content = null ) {
  // do something to $content
  // always return
  return $content;
}
add_shortcode( 'wporg', 'wporg_shortcode' );
```

Looking at the callback function we see that we chose to accept two parameters, `$atts` and `$content`. The `$content` parameter is going to hold our enclosed content. We will talk about `$atts` later.

The default value of `$content` is set to `null` so we can differentiate between a self-closing tag and enclosing tags by using PHP function [`is_null()`](https://www.php.net/manual/en/function.is-null.php).

The shortcode `[$tag]`, including its content and the end `[/$tag]` will be replaced with the **return value** of the handler function.

[alert]It is the responsibility of the handler function to [secure the output](https://developer.wordpress.org/plugins/security/securing-output/).[/alert]

## Shortcode-ception

The shortcode parser performs a **single pass** on the content of the post.

This means that if the `$content` parameter of a shortcode handler contains another shortcode, it won’t be parsed. In this example, `[shortcode]` will not be processed:

```
[wporg]another [shortcode] is included[/wporg]
```

Using shortcodes inside other shortcodes is possible by calling `do_shortcode()` on the **final return value** of the handler function.

```
function wporg_shortcode( $atts = array(), $content = null ) {
  // do something to $content
  // run shortcode parser recursively
  $content = do_shortcode( $content );
  // always return
  return $content;
}
add_shortcode( 'wporg', 'wporg_shortcode' );
```

## Limitations

The shortcode parser is unable to handle mixing of enclosing and non-enclosing forms of the same `[$tag]`.

```
[wporg] non-enclosed content [wporg]enclosed content[/wporg]
```

Instead of being treated as two shortcodes separated by the text "`non-enclosed content`", the parser treats this as a single shortcode enclosing "`non-enclosed content [wporg]enclosed content`".

# 带参数的短代码

现在我们已经了解了如何创建[基本短代码](https://developer.wordpress.org/plugins/shortcodes/basic-shortcodes/)以及如何将其用作[自闭合和包裹式短代码](https://developer.wordpress.org/plugins/shortcodes/enclosing-shortcodes/)，接下来我们将探讨如何在短代码 `[$tag]` 和处理函数中使用参数。

短代码 `[$tag]` 可以接受参数，这些参数被称为属性：

```
[wporg title="WordPress.org"]
  享受使用 WordPress.org 短代码的乐趣。
[/wporg]
```

短代码处理函数可以接受 3 个参数：

- `$atts` – 数组 – `[$tag]` 的属性
- `$content` – 字符串 – 短代码内部的内容。在上面的示例中，它将是“享受使用 WordPress.org 短代码的乐趣”。
- `$tag` – 字符串 – `[$tag]` 的名称（即短代码的名称）

```
function wporg_shortcode( $atts = array(), $content = null, $tag = '' ) {}
```

## 解析属性

对用户而言，短代码只是文章内容中带方括号的字符串。用户并不清楚哪些属性可用，也不了解其背后的运行机制。

对插件开发者来说，无法强制规范属性的使用方式。用户可能包含一个属性、两个属性，或完全不包含任何属性。

要掌控短代码的使用方式，可采取以下措施：

- 为处理函数声明默认参数
- 使用 [`array_change_key_case()`](https://www.php.net/manual/en/function.array-change-key-case.php) 对属性数组的键名进行大小写规范化处理
- 通过 [`shortcode_atts()`](https://developer.wordpress.org/reference/functions/shortcode_atts/) 解析属性，需提供默认值数组和用户 `$atts` 参数
- 在返回输出前进行[安全转义处理](https://developer.wordpress.org/apis/security/escaping/)

## Complete Example

Complete example using a basic shortcode structure, taking care of self-closing and enclosing scenarios and securing output.

A `[wporg]` shortcode that will accept a title and will display a box that we can style with CSS.

```
/**
 * The [wporg] shortcode.
 *
 * Accepts a title and will display a box.
 *
 * @param array  $atts    Shortcode attributes. Default empty.
 * @param string $content Shortcode content. Default null.
 * @param string $tag     Shortcode tag (name). Default empty.
 * @return string Shortcode output.
 */
function wporg_shortcode( $atts = [], $content = null, $tag = '' ) {
  // normalize attribute keys, lowercase
  $atts = array_change_key_case( (array) $atts, CASE_LOWER );

  // override default attributes with user attributes
  $wporg_atts = shortcode_atts(
    array(
      'title' => 'WordPress.org',
    ), $atts, $tag
  );

  // start box
  $o = '<div class="wporg-box">';

  // title
  $o .= '<h2>' . esc_html( $wporg_atts['title'] ) . '</h2>';

  // enclosing tags
  if ( ! is_null( $content ) ) {
    // $content here holds everything in between the opening and the closing tags of your shortcode. eg.g [my-shortcode]content[/my-shortcode].
    // Depending on what your shortcode supports, you will parse and append the content to your output in different ways.
    // In this example, we just secure output by executing the_content filter hook on $content.
    $o .= apply_filters( 'the_content', $content );
  }

  // end box
  $o .= '</div>';

  // return output
  return $o;
}

/**
 * Central location to create all shortcodes.
 */
function wporg_shortcodes_init() {
  add_shortcode( 'wporg', 'wporg_shortcode' );
}

add_action( 'init', 'wporg_shortcodes_init' );
```

# TinyMCE 增强型短代码

可以在 TinyMCE 可视化编辑器中解析短代码，使其渲染实际内容而非短代码本身。

切换到 `文本` 标签页可再次查看实际短代码。

以下是使用此功能的 WordPress 内置短代码。

## 音频短代码

`[audio]` 短代码允许嵌入单个音频文件。

![音频短代码](https://i3.wp.com/developer.wordpress.org/files/2014/09/shortcodes-tinymce-enhanced-shortcodes-01.png)

## 标题短代码

`[caption]` 短代码将图像包裹在 div 中，并在标题周围放置 `<p class="wp-caption-text">` 标签。

![标题短代码](https://i3.wp.com/developer.wordpress.org/files/2014/09/shortcodes-tinymce-enhanced-shortcodes-02.png)

## 图库短代码

`[gallery]` 短代码允许在 div 中一次性嵌入多张图像。

![图库短代码](https://i3.wp.com/developer.wordpress.org/files/2014/09/shortcodes-tinymce-enhanced-shortcodes-03.png)

## 播放列表短代码

`[playlist]` 短代码允许附加多个媒体文件，并通过 html5 播放列表进行渲染。

![播放列表短代码](https://i3.wp.com/developer.wordpress.org/files/2014/09/shortcodes-tinymce-enhanced-shortcodes-04.png)

## 视频短代码

`[video]` 短代码与 `[audio]` 短代码非常相似；它只是渲染视频而非音频。

![视频短代码](https://i3.wp.com/developer.wordpress.org/files/2014/09/shortcodes-tinymce-enhanced-shortcodes-05.png)

# Index

## 分类法

**分类法** 是对事物进行分类/分组的专业术语。分类法可以是层级结构（包含父级/子级）或扁平结构。

WordPress 将分类法存储在 `term_taxonomy` 数据库表中，允许开发者注册自定义分类法，与现有分类法并存。

分类法包含 **术语**，这些术语作为对事物进行分类/分组的话题。它们存储在 `terms` 表中。

例如：一个名为 "艺术" 的分类法可以包含多个术语，例如 "现代" 和 "18世纪"。

本章将展示如何注册自定义分类法，如何从数据库中检索其内容，以及如何向公众展示它们。

[info]WordPress 3.4 及更早版本有一个名为 "链接" 的分类法，该分类法在 WordPress 3.5 中已被弃用。[/info]

# 术语拆分 (WordPress 4.2)

此处信息仅为历史记录。如果您对 2015 年之前术语的工作方式不感兴趣，可以跳过本节。

## WordPress 4.2 之前

不同分类法中具有相同缩略名的术语共享同一个术语 ID。例如，缩略名为 "news" 的标签和类别拥有相同的术语 ID。

## WordPress 4.2+

从 4.2 版本开始，当这些共享分类项中的一个被更新时，它将被拆分：更新后的分类项将被分配一个新的分类项 ID。

## 这对您意味着什么？

在绝大多数情况下，此更新是无缝且顺利的。然而，一些在选项、文章元数据、用户元数据或其他地方存储分类项 ID 的插件和主题可能受到了影响。

## 处理拆分

WordPress 4.2 包含两种不同的工具，以帮助插件和主题作者进行过渡。

### `split_shared_term` 钩子

当一个共享术语被分配新的术语 ID 时，会触发一个新的 `split_shared_term` 动作。

以下是插件和主题作者如何利用此钩子来确保存储的术语 ID 得到更新的几个示例。

#### Term ID stored in an option

Let's say your plugin stores an option called `featured_tags` that contains an array of term IDs (`[4, 6, 10]`) that serve as the query parameter for your homepage featured posts section.

In this example, you'll hook to `split_shared_term` action, check whether the updated term ID is in the array, and update if necessary.

```
/**
 * Update featured_tags option when a shared term gets split.
 *
 * @param int    $term_id          ID of the formerly shared term.
 * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
 * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
 * @param string $taxonomy         Taxonomy for the split term.
 */
function wporg_featured_tags_split( int $term_id, int $new_term_id, int $term_taxonomy_id, string $taxonomy ): void {
  // we only care about tags, so we'll first verify that the taxonomy is post_tag.
  if ( 'post_tag' === $taxonomy ) {

    // get the currently featured tags.
    $featured_tags = get_option( 'featured_tags' );

    // if the updated term is in the array, note the array key.
    $found_term = array_search( $term_id, $featured_tags, true );
    if ( false !== $found_term ) {

      // the updated term is a featured tag! replace it in the array, save the new array.
      $featured_tags[ $found_term ] = $new_term_id;
      update_option( 'featured_tags', $featured_tags );
    }
  }
}
add_action( 'split_shared_term', 'wporg_featured_tags_split', 10, 4 );
```

#### Term ID stored in post meta

Let's say your plugin stores a term ID in post meta for pages so that you can show related posts for a certain page.

In this case, you need to use the `get_posts()` function to get the pages with your `meta_key` and update the `meta_value` matching the split term ID.

```
/**
 * Update related posts term ID for pages
 *
 * @param int    $term_id          ID of the formerly shared term.
 * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
 * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
 * @param string $taxonomy         Taxonomy for the split term.
 */
function wporg_page_related_posts_split( int $term_id, int $new_term_id, int $term_taxonomy_id, string $taxonomy ): void {
  // find all the pages where meta_value matches the old term ID.
  $page_ids = get_posts(
    array(
      'post_type'  => 'page',
      'fields'     => 'ids',
      'meta_key'   => 'meta_key',
      'meta_value' => $term_id,
    )
  );

  // if such pages exist, update the term ID for each page.
  if ( $page_ids ) {
    foreach ( $page_ids as $id ) {
      update_post_meta( $id, 'meta_key', $new_term_id, $term_id );
    }
  }
}
add_action( 'split_shared_term', 'wporg_page_related_posts_split', 10, 4 );
```

### The `wp_get_split_term` function

[info]Using the `split_shared_term` hook is the preferred method for processing Term ID changes.

However, there may be cases where Terms are split without your plugin having a chance to hook to the `split_shared_term` action.[/info]

WordPress 4.2 stores information about taxonomy terms that have been split, and provides the `wp_get_split_term()` utility function to help developers retrieve this information.

Consider the case above, where your plugin stores term IDs in an option named `featured_tags`. You may want to build a function that validates these tag IDs (perhaps to be run on plugin update), to be sure that none of the featured tags has been split:

```
/**
 * Retrieve information about split terms and udpates the featured_tags option with the new term IDs.
 *
 * @return void
 */
function wporg_featured_tags_check_split() {
  $featured_tag_ids = get_option( 'featured_tags', array() );

  // check to see whether any IDs correspond to post_tag terms that have been split.
  foreach ( $featured_tag_ids as $index => $featured_tag_id ) {
    $new_term_id = wp_get_split_term( $featured_tag_id, 'post_tag' );

    if ( $new_term_id ) {
      $featured_tag_ids[ $index ] = $new_term_id;
    }
  }

  // save
  update_option( 'featured_tags', $featured_tag_ids );
}
```

Note that `wp_get_split_term()` takes two parameters, `$old_term_id` and `$taxonomy` and returns an integer.

If you need to retrieve a list of all split terms associated with an old Term ID, regardless of taxonomy, use `wp_get_split_terms()`.

# 使用自定义分类法

## 分类法简介

要了解什么是分类法及其功能，请阅读[分类法](https://developer.wordpress.org/plugins/taxonomies/)介绍。

## 自定义分类法

就分类系统而言，“分类目录”和“标签”的结构性不强，因此开发者创建自己的分类法可能更为有利。

WordPress 允许开发者创建**自定义分类法**。当需要创建独特的命名系统，并以可预测的方式在后台访问它们时，自定义分类法非常有用。

## 为何使用自定义分类法？

你可能会问："既然可以用分类和标签来组织内容，为什么还要费心创建自定义分类法呢？"

嗯……让我们举个例子。假设我们有一位客户是厨师，她想让你创建一个展示原创菜谱的网站。

一种组织网站的方式是：创建一个名为"菜谱"的自定义文章类型来存储她的食谱，然后创建一个"餐点类型"分类法来区分"开胃菜"和"甜点"，最后再创建一个"食材"分类法来区分"鸡肉"和"巧克力"类菜肴。

这些分组*原本*可以用分类或标签来定义——你可以创建一个"餐点类型"分类，下设"开胃菜"和"甜点"子分类；再创建一个"食材"分类，为每种食材设置子分类。

使用自定义分类法的主要优势在于：你可以独立于常规分类和标签来引用"餐点类型"和"食材"。它们甚至在管理后台拥有独立的菜单。

此外，创建自定义分类法还能让你构建定制化界面，这将简化客户的操作流程，并使数据录入过程更符合其业务特性。

现在想象一下：如果这些自定义分类法和界面被实现在一个插件中——你就构建了一个可用于任何WordPress网站的菜谱插件。

## 示例：课程分类法

以下示例将展示如何创建一个插件，为默认的 `post` 文章类型添加自定义分类法“课程”。请注意，添加自定义分类法的代码不必在独立的插件中；如果需要，它可以包含在主题中或作为现有插件的一部分。

在尝试创建自己的插件之前，请务必阅读 [插件基础](https://developer.wordpress.org/plugins/plugin-basics/) 章节。

### 步骤 1：准备工作

前往 **文章 > 写文章** 页面。您会注意到目前只有“分类目录”和“标签”。

![尚无自定义分类法元框](https://make.wordpress.org/docs/files/2014/02/no-custom-taxonomy-meta-box.png)

## Step 2: Creating a New Plugin

Register the Taxonomy "course" for the post type "post" using the `init` action hook.

```
/*
 * Plugin Name: Course Taxonomy
 * Description: A short example showing how to add a taxonomy called Course.
 * Version: 1.0
 * Author: developer.wordpress.org
 * Author URI: https://codex.wordpress.org/User:Aternus
 */

function wporg_register_taxonomy_course() {
  $labels = array(
    'name'              => _x( 'Courses', 'taxonomy general name' ),
    'singular_name'     => _x( 'Course', 'taxonomy singular name' ),
    'search_items'      => __( 'Search Courses' ),
    'all_items'         => __( 'All Courses' ),
    'parent_item'       => __( 'Parent Course' ),
    'parent_item_colon' => __( 'Parent Course:' ),
    'edit_item'         => __( 'Edit Course' ),
    'update_item'       => __( 'Update Course' ),
    'add_new_item'      => __( 'Add New Course' ),
    'new_item_name'     => __( 'New Course Name' ),
    'menu_name'         => __( 'Course' ),
  );
  $args   = array(
    'hierarchical'      => true, // make it hierarchical (like categories)
    'labels'            => $labels,
    'show_ui'           => true,
    'show_admin_column' => true,
    'query_var'         => true,
    'rewrite'           => [ 'slug' => 'course' ],
  );
  register_taxonomy( 'course', [ 'post' ], $args );
}
add_action( 'init', 'wporg_register_taxonomy_course' );
```

### 步骤 3：查看结果

激活你的插件，然后转到 **文章 > 写文章**。你应该会看到一个用于“课程”分类法的新元框。

![课程分类法文章界面](https://make.wordpress.org/docs/files/2014/02/courses_taxonomy_post_screen.png)

### 代码解析

以下讨论将分解上述代码，描述其功能与参数。

函数 `wporg_register_taxonomy_course` 包含了注册自定义分类法所需的所有步骤。

`$labels` 数组存储了自定义分类法的标签。

这些标签将用于在管理区域显示关于该分类法的各种信息。

`$args` 数组存储了创建自定义分类法时将使用的配置选项。

函数 [`register_taxonomy()`](https://developer.wordpress.org/reference/functions/register_taxonomy/) 使用 `$args` 数组进行配置，为 `post` 文章类型创建了一个标识符为 `course` 的新分类法。

函数 [`add_action()`](https://developer.wordpress.org/reference/functions/add_action/) 将 `wporg_register_taxonomy_course` 函数的执行绑定到 `init` 动作钩子上。

#### $args

$args 数组保存了自定义分类法的重要配置，它指示 WordPress 该分类法应如何工作。

### 总结

查看 [`register_taxonomy()`](https://developer.wordpress.org/reference/functions/register_taxonomy/) 函数以获取完整参数列表及各参数作用说明。

以我们的课程分类法为例，WordPress 将自动为 `course` 分类法创建归档页面和子页面。

归档页面位于 `/course/`，其下将根据术语别名生成子页面（`/course/%%term-slug%%/`）。

## 使用您的分类法

WordPress 提供了**许多**用于与您的自定义分类法及其内部术语进行交互的函数。

以下是一些示例：

- `the_terms`：接受一个分类法参数，并以列表形式渲染术语。
- `wp_tag_cloud`：接受一个分类法参数，并渲染术语的标签云。
- `is_taxonomy`：允许您判断给定的分类法是否存在。

# 用户

_用户_ 是 WordPress 安装中具有相应权限的访问账户。每个 WordPress 用户至少拥有用户名、密码和电子邮件地址。

用户账户创建后，该用户即可通过 WordPress 管理后台（或以编程方式）登录，以访问 WordPress 功能和数据。WordPress 将用户存储在 `users` 表中。

## 角色与权限

用户被分配[角色](https://developer.wordpress.org/plugins/users/roles-and-capabilities/#roles)，每个角色拥有一组[权限](https://developer.wordpress.org/plugins/users/roles-and-capabilities/#capabilities)。

您可以创建具有自定义权限集的新角色。也可以创建自定义权限，并将其分配给现有角色或新角色。

在 WordPress 中，开发者可以利用用户角色来限制账户可执行的操作集合。

## 最小权限原则

WordPress 遵循最小权限原则，即仅授予用户执行所需工作所必需的权限。您应尽可能遵循此原则，在适当时创建角色，并在执行敏感任务前检查权限。

# 角色与权限

角色与权限是 WordPress 中两个重要的概念，它们允许您控制用户特权。

WordPress 将角色及其对应的权限存储在 `options` 表中，键名为 `user_roles`。

## 角色

角色定义了用户的一组权限。例如，用户在其仪表板中可以看到和操作的内容。

**默认情况下，WordPress 有六个角色：**

- 超级管理员
- 管理员
- 编辑
- 作者
- 投稿者
- 订阅者

可以添加更多角色，也可以移除默认角色。

![用户角色](https://i3.wp.com/developer.wordpress.org/files/2014/09/wp-roles.png)

### 添加角色

使用 [`add_role()`](https://developer.wordpress.org/reference/functions/add_role/) 添加新角色并为其分配权限。

```
function wporg_simple_role() {
  add_role(
    'simple_role',
    'Simple Role',
    array(
      'read'         => true,
      'edit_posts'   => true,
      'upload_files' => true,
    ),
  );
}

// 添加 simple_role。
add_action( 'init', 'wporg_simple_role' );
```

[alert]首次调用 [`add_role()`](https://developer.wordpress.org/reference/functions/add_role/) 后，角色及其权限将被存储到数据库中！

后续调用将不会产生任何效果：包括修改权限列表，这可能不符合你的预期行为。[/alert]

[info]要批量修改权限列表：请使用 [`remove_role()`](https://developer.wordpress.org/reference/functions/remove_role/) 移除角色，然后使用 [`add_role()`](https://developer.wordpress.org/reference/functions/add_role/) 重新添加并指定新的权限。

请确保仅在权限与预期不符时才执行此操作（即添加条件判断），否则会显著降低性能！[/info]

### 移除角色

使用 [`remove_role()`](https://developer.wordpress.org/reference/functions/remove_role/) 移除角色。

```
function wporg_simple_role_remove() {
  remove_role( 'simple_role' );
}

// 移除 simple_role。
add_action( 'init', 'wporg_simple_role_remove' );
```

[alert]首次调用 [`remove_role()`](https://developer.wordpress.org/reference/functions/remove_role/) 后，该角色及其权限将从数据库中移除！

后续调用将不会产生任何效果。[/alert]

[info]如果你要移除默认角色：

- 我们**不建议**移除管理员和超级管理员角色！
- 请务必将相关代码保留在你的插件/主题中，因为未来的 WordPress 更新可能会重新添加这些角色。
- 运行 `update_option('default_role', YOUR_NEW_DEFAULT_ROLE)`，因为你将删除 WordPress 的默认角色 `subscriber`。[/info]

## Capabilities

Capabilities define what a **role** can and can not do: edit posts, publish posts, etc.

[info]Custom post types can require a certain set of Capabilities.[/info]

### Adding Capabilities

You may define new capabilities for a role.

Use [`get_role()`](https://developer.wordpress.org/reference/functions/get_role/) to get the role object, then use the `add_cap()` method of that object to add a new capability.

```
function wporg_simple_role_caps() {
  // Gets the simple_role role object.
  $role = get_role( 'simple_role' );

  // Add a new capability.
  $role->add_cap( 'edit_others_posts', true );
}

// Add simple_role capabilities, priority must be after the initial role definition.
add_action( 'init', 'wporg_simple_role_caps', 11 );
```

[info]It's possible to add custom capabilities to any role.

Under the default WordPress admin, they would have no effect, but they can be used for custom admin screen and front-end areas.[/info]

### 移除权限

您可以从角色中移除权限。

实现方式与添加权限类似，区别在于使用角色对象的 `remove_cap()` 方法。

## 使用角色与权限

### 获取角色

通过 [`get_role()`](https://developer.wordpress.org/reference/functions/get_role/) 获取包含其所有权限的角色对象。

### 用户权限检查

使用 [`user_can()`](https://developer.wordpress.org/reference/functions/user_can/) 可检查用户是否拥有指定的**角色**或**权限**。

```
user_can( $user, $capability );
```

[warning]存在未记录的第三个参数 $args，可包含用于权限测试的目标对象。

例如：传递文章 ID 以测试对该特定文章的权限。[/warning]

### 当前用户权限检查

[`current_user_can()`](https://developer.wordpress.org/reference/functions/current_user_can/) 是 [`user_can()`](https://developer.wordpress.org/reference/functions/user_can/) 的封装函数，它将当前用户对象作为 `$user` 参数传入。

当后台和前台区域需要特定权限级别才能访问或修改时，可使用此函数。

```
current_user_can( $capability );
```

### 示例

以下是一个在模板文件中添加编辑链接的实际示例，前提是用户具备相应权限：

```
if ( current_user_can( 'edit_posts' ) ) {
  edit_post_link( esc_html__( 'Edit', 'wporg' ), '<p>', '</p>' );
}
```

## 多站点

[`current_user_can_for_blog()`](https://developer.wordpress.org/reference/functions/current_user_can_for_blog/) 函数用于测试当前用户在特定博客上是否拥有某个**角色**或**权限**。

```
current_user_can_for_blog( $blog_id, $capability );
```

## 参考

[用户角色与权限](https://wordpress.org/documentation/article/roles-and-capabilities/)的 Codex 参考文档。

# 使用用户元数据

## 简介

WordPress 的 `users` 表最初设计为仅包含用户的基本信息。

[info]截至 WP 4.7，该表包含：`ID`、`user_login`、`user_pass`、`user_nicename`、`user_email`、`user_url`、`user_registered`、`user_activation_key`、`user_status` 和 `display_name`。[/info]

因此，为了存储额外数据，引入了 `usermeta` 表，它可以存储关于用户的任意数量数据。

这两个表通过基于 `users` 表中 `ID` 的一对多关系相互关联。

## 操作用户元数据

主要有两种方式操作用户元数据。

1. 用户资料屏幕中的表单字段。
2. 通过函数调用以编程方式。

### 通过表单字段

表单字段选项适用于用户可以访问 WordPress 管理后台的情况，用户将能够在其中查看和编辑个人资料。

在深入示例之前，了解此过程中涉及的钩子及其存在的原因非常重要。

#### `show_user_profile` 钩子

当用户编辑**其自身**用户资料时，会触发此操作钩子。

**请注意，** 不具备编辑自身资料权限的用户不会触发此钩子。

#### `edit_user_profile` 钩子

当用户编辑**其他用户**的个人资料时，会触发此操作钩子。

**请注意，** 不具备编辑第三方资料权限的用户不会触发此钩子。

#### Example Form Field

In the example below we will be adding a birthday field to the all profile screens. Saving it to the database on profile updates.

```
/**
 * The field on the editing screens.
 *
 * @param $user WP_User user object
 */
function wporg_usermeta_form_field_birthday( $user ) {
  ?>
  <h3>It's Your Birthday</h3>
  <table class="form-table">
    <tr>
      <th>
        <label for="birthday">Birthday</label>
      </th>
      <td>
        <input type="date"
          class="regular-text ltr"
          id="birthday"
          name="birthday"
          value="<?= esc_attr( get_user_meta( $user->ID, 'birthday', true ) ) ?>"
          title="Please use YYYY-MM-DD as the date format."
          pattern="(19[0-9][0-9]|20[0-9][0-9])-(1[0-2]|0[1-9])-(3[01]|[21][0-9]|0[1-9])"
          required>
        <p class="description">
          Please enter your birthday date.
        </p>
      </td>
    </tr>
  </table>
  <?php
}

/**
 * The save action.
 *
 * @param $user_id int the ID of the current user.
 *
 * @return bool Meta ID if the key didn't exist, true on successful update, false on failure.
 */
function wporg_usermeta_form_field_birthday_update( $user_id ) {
  // check that the current user have the capability to edit the $user_id
  if ( ! current_user_can( 'edit_user', $user_id ) ) {
    return false;
  }

  // create/update user meta for the $user_id
  return update_user_meta(
    $user_id,
    'birthday',
    $_POST['birthday']
  );
}

// Add the field to user's own profile editing screen.
add_action(
  'show_user_profile',
  'wporg_usermeta_form_field_birthday'
);

// Add the field to user profile editing screen.
add_action(
  'edit_user_profile',
  'wporg_usermeta_form_field_birthday'
);

// Add the save action to user's own profile editing screen update.
add_action(
  'personal_options_update',
  'wporg_usermeta_form_field_birthday_update'
);

// Add the save action to user profile editing screen update.
add_action(
  'edit_user_profile_update',
  'wporg_usermeta_form_field_birthday_update'
);
```

### 以编程方式

此选项适用于构建自定义用户区域和/或计划禁用 WordPress 管理区域访问权限的情况。

可用于操作用户元数据的函数包括：[`add_user_meta()`](https://developer.wordpress.org/reference/functions/add_user_meta/)、[`update_user_meta()`](https://developer.wordpress.org/reference/functions/update_user_meta/)、[`delete_user_meta()`](https://developer.wordpress.org/reference/functions/delete_user_meta/) 和 [`get_user_meta()`](https://developer.wordpress.org/reference/functions/get_user_meta/)。

#### 添加

```
add_user_meta(
  int $user_id,
  string $meta_key,
  mixed $meta_value,
  bool $unique = false
);
```

关于所用参数的完整说明，请参阅关于 [`add_user_meta()`](https://developer.wordpress.org/reference/functions/add_user_meta/) 的函数参考。

#### 更新

```
update_user_meta(
  int $user_id,
  string $meta_key,
  mixed $meta_value,
  mixed $prev_value = ''
);
```

关于所用参数的完整说明，请参阅 [`update_user_meta()`](https://developer.wordpress.org/reference/functions/update_user_meta/) 的函数参考。

#### 删除

```
delete_user_meta(
  int $user_id,
  string $meta_key,
  mixed $meta_value = ''
);
```

关于所用参数的完整说明，请参阅关于 [`delete_user_meta()`](https://developer.wordpress.org/reference/functions/delete_user_meta/) 的函数参考。

#### 获取

```
get_user_meta(
  int $user_id,
  string $key = '',
  bool $single = false
);
```

关于所用参数的完整说明，请参阅 [`get_user_meta()`](https://developer.wordpress.org/reference/functions/get_user_meta/) 的函数参考文档。

请注意，如果仅传递 `$user_id` 参数，该函数将检索所有元数据并以关联数组形式返回。

您可以在插件或主题的任何位置渲染用户元数据。

# 用户管理

## 添加用户

您可以使用 `wp_create_user()` 或 `wp_insert_user()` 来添加用户。

`wp_create_user()` 仅使用用户名、密码和邮箱参数创建用户，而 `wp_insert_user()` 接受描述用户及其属性的数组或对象。

### 创建用户

`wp_create_user()` 允许您创建新的 WordPress 用户。

[info]它使用 [`wp_slash()`](https://developer.wordpress.org/reference/functions/wp_slash/) 对值进行转义，使用 PHP 的 compact() 函数创建包含这些值的数组，并通过 [`wp_insert_user()`](https://developer.wordpress.org/reference/functions/wp_insert_user/) 执行插入操作。[/info]

请查阅关于 `wp_create_user()` 的函数参考以获取所用参数的完整说明。

#### Example Create

```
// check if the username is taken
$user_id = username_exists( $user_name );

// check that the email address does not belong to a registered user
if ( ! $user_id && email_exists( $user_email ) === false ) {
  // create a random password
  $random_password = wp_generate_password( 12, false );
  // create the user
  $user_id = wp_create_user(
    $user_name,
    $random_password,
    $user_email
  );
}
```

## 插入用户

```
wp_insert_user( $userdata );
```

[info]该函数会对大多数预定义属性调用过滤器。

创建用户时（用户 ID 不存在），该函数会执行 `user_register` 操作。

更新用户时（用户 ID 存在），该函数会执行 `profile_update` 操作。[/info]

关于所用参数的完整说明，请参阅 `wp_insert_user()` 的函数参考文档。

### Example Insert

Below is an example showing how to insert a new user with the website profile field filled in.

```
$username  = $_POST['username'];
$password  = $_POST['password'];
$website   = $_POST['website'];
$user_data = [
  'user_login' => $username,
  'user_pass'  => $password,
  'user_url'   => $website,
];

$user_id = wp_insert_user( $user_data );

// success
if ( ! is_wp_error( $user_id ) ) {
  echo 'User created: ' . $user_id;
}
```

## 更新用户

`wp_update_user()` 用于更新数据库中的单个用户。更新数据通过 `$userdata` 数组/对象传递。

如需更新单个用户元数据，请改用 `update_user_meta()`。如需创建新用户，请改用 `wp_insert_user()`。

[info]如果当前用户的密码被更新，cookies 将被清除！[/info]

关于所用参数的完整说明，请参阅 `wp_update_user()` 的函数参考文档。

### Example Update

Below is an example showing how to update a user's website profile field.

```
$user_id = 1;
$website = 'https://wordpress.org';

$user_id = wp_update_user(
  array(
    'ID'       => $user_id,
    'user_url' => $website,
  )
);

if ( is_wp_error( $user_id ) ) {
  // error
} else {
  // success
}
```

## 删除用户

`wp_delete_user()` 用于删除用户，并可选择将关联实体重新分配给另一个用户 ID。

[info]该函数在用户被删除后执行 `deleted_user` 操作。[/info]

[alert]如果 `$reassign` 参数未设置为有效的用户 ID，则属于被删除用户的所有实体都将被删除！[/alert]

请参阅关于 `wp_delete_user()` 的函数参考，以获取所用参数的完整说明。

# Index

## WordPress.org 插件目录

WordPress.org 为所有希望在我们目录中开发插件的开发者提供免费托管服务。

所有托管于此的插件均可：

- 查看统计信息（另请参阅 [WordPress.org 插件 API](https://codex.wordpress.org/WordPress.org_API#Plugins)）。
- 接收用户的反馈与评价。
- 通过免费论坛提供支持。

[tip]请务必查阅 [开发者常见问题解答！](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/)[/tip]

同时请确保您已了解插件团队在审核插件时通常遇到的[常见问题](wordpress-org/common-issues/index)。

如果您在 WordPress.org 托管方面需要帮助，可以通过 Slack 在 `#pluginreview` 频道联系插件团队。

## 要求

简要概述：

- 插件必须兼容 [GNU 通用公共许可证 v2](https://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses) 或更高版本。若未指定许可证，代码将被视为“GPLv2 或更高版本”。
- 提供的 [Subversion](https://subversion.apache.org/) 仓库必须仅用于功能性 WordPress 插件。
- 必须遵守版权和商标法。
- 插件和开发者不得从事任何非法、不诚实或道德上令人反感的行为，包括垃圾邮件或骚扰。

[alert]所有插件和开发者必须遵守我们的[详细插件指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)。[/alert]

## How to...

If you're just getting started, it helps to know how to submit your plugin, use SVN, and so on.

- [...plan, submit, and maintain your plugin](https://developer.wordpress.org/plugins/wordpress-org/planning-submitting-and-maintaining-plugins/)
- [...use SVN (aka Subversion)](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/)
- [...manage your readme.txt](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/)
- [...write proper plugin headers](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/)
- [...use plugin assets (header images and icons)](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/)
- [...take over an existing plugin](https://developer.wordpress.org/plugins/wordpress-org/take-over-an-existing-plugin/)
- [...use the support forums](https://developer.wordpress.org/plugins/wordpress-org/using-the-forums/)
- [...grant users special roles (contributor, supporter, committer, etc)](https://developer.wordpress.org/plugins/wordpress-org/special-user-roles-capabilities/)
- [...transfer your plugin to a new owner](https://developer.wordpress.org/plugins/wordpress-org/transferring-your-plugin-to-a-new-owner/)
- [...add your plugin to the block directory](https://developer.wordpress.org/plugins/wordpress-org/add-your-plugin-to-the-block-directory/)
- [...manage your plugin's security](https://developer.wordpress.org/plugins/wordpress-org/plugin-security/)
- [...report an insecure plugin](https://developer.wordpress.org/plugins/wordpress-org/plugin-security/reporting-plugin-security-issues/)

# 将您的插件添加到区块目录

区块相关插件主要有两种目录列表：

- **仅包含**区块的插件
- 包含区块的插件

一个插件可以同时出现在两个列表中，但这不会对您的插件 SEO 或排名带来任何好处。成为仅包含区块的插件的唯一好处是，您可以在区块编辑器本身的区块目录中添加插件，以便立即安装。

## 仅区块插件

仅区块插件是指**仅**包含区块的插件。

区块插件需要比常规 WordPress 插件更小巧、更精简，以确保能够安全地一键安装。这意味着除了遵守常规的[插件指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)外，您还需要遵循一些额外的规则。特别是，您应主要使用 JavaScript 代码，将 PHP 保持在最低限度；并且不要在古腾堡编辑器之外添加任何用户界面或其他代码。

如果您是某个区块插件的提交者，并且该插件已通过检查工具确认符合添加到[区块目录](https://wordpress.org/plugins/browse/block/)的标准，那么您可以[使用区块检查工具](https://wordpress.org/plugins/developers/block-plugin-validator/)自行添加：

![区块检查工具界面，带有“将插件名称添加到区块目录”按钮](https://i0.wp.com/developer.wordpress.org/files/2020/08/Screen-Shot-2020-07-10-at-1.29.25-pm.png?resize=1024%2C308&ssl=1)

同样，如果您发现问题或希望不将其包含在内，也可以随时使用同一工具将其移除。

**有用的工具：**

- [区块创建教程](https://github.com/WordPress/gutenberg/pull/22831/files?short_path=c4d2c28#diff-c4d2c286eac33acdc7571032a984e0ca) – 如何编写区块插件
- [区块提交教程](https://github.com/WordPress/gutenberg/pull/23545/files?short_path=555f1c3#diff-555f1c31856d86ed5ff0d492b5a127c1) – 确保您的区块已准备好加入区块目录的技巧和建议
- [区块检查工具](https://make.wordpress.org/plugins/2020/07/11/you-can-now-add-your-own-plugins-to-the-block-directory/) – 验证插件是否符合收录条件

## 包含区块的插件

许多旧版插件以及更大型、更复杂的插件可能包含区块。它们通常也包含其他功能，例如小工具。这类插件的典型代表是 Jetpack 或 Yoast SEO。虽然它们拥有大量功能，但也包含一些区块。

如果您编写的插件引入或改进了区块，或知道有此类插件，**请发送邮件至 [plugins@wordpress.org](wordpress-org/add-your-plugin-to-the-block-directory/mailto:plugins@wordpress.org)** 并申请将您的插件加入列表。届时我们将审核您的插件以确认请求，同时确保其符合所有现行规范标准。

发送邮件前，请务必确认您的插件与最新版 WordPress 兼容，且不存在任何安全问题。如果插件存在安全或规范问题，可能会在您修正前被暂停处理。

# 警报与警告

当您访问 WordPress.org 上的插件页面时，可能会注意到特殊的警报或警告。这些提示旨在帮助访客了解各个插件的状态。

## 已批准与待处理数据

![蓝色背景 - 此插件已获批准并等待数据上传，但尚未对公众可见。一旦您进行首次提交，插件将变为公开状态。](https://developer.wordpress.org/files/2018/02/approved.jpg)

已获批准但尚未上传代码的插件将显示此消息：此信息**仅**对插件所有者显示，一旦通过 SVN 推送代码后便会消失。

## 已关闭

自 2017 年 11 月起，已关闭的插件会显示如下通知：

![红色背景：此插件已关闭，不再提供下载。](https://i3.wp.com/developer.wordpress.org/files/2018/02/closed.png)

所有访客均可看到此提示，表明插件已被关闭。2018 年 1 月之后关闭的插件将包含日期：

![红色背景：此插件已于 2018 年 2 月 7 日关闭，不再提供下载。](https://i3.wp.com/developer.wordpress.org/files/2018/02/closed-alt.jpg)

60 天后，提示信息将更新以说明插件被关闭的 _原因_：

![详细说明插件关闭原因的提示](https://i3.wp.com/developer.wordpress.org/files/2018/02/why-closed.png)

插件提交者将看到以下附加说明：

![蓝色背景：如果您未请求此变更，请联系 plugins@wordpress.org 了解状态。插件关闭时，所有具有提交权限的开发者都会收到通知及原因说明，请同时检查您的垃圾邮件。](https://i3.wp.com/developer.wordpress.org/files/2018/02/closed-owner.png)

### 插件被关闭的原因

- 作者要求 – 作者主动请求关闭插件。
- 违反准则 – 违反了任何一项指导原则。
- 许可/商标违规 – 使用了非 GPL 代码，或商标被滥用。
- 已并入核心 – 该插件现已成为核心的一部分（仅限功能项目）。
- 安全问题 – 在该插件中发现了安全隐患。

除非在极端情况下，否则不会向 WordPress.org 安全团队或插件作者之外的任何人提供插件被关闭的更多细节。

## 已过时

不支持最近 3 个主要 WordPress 版本的插件会显示以下通知：

![黄色背景：此插件尚未与最新 3 个主要 WordPress 版本进行测试。它可能不再被维护或支持，并且在较新版本的 WordPress 中使用时可能存在兼容性问题。](https://i3.wp.com/developer.wordpress.org/files/2018/02/old.jpg)

此前，此消息用于提醒用户插件在过去 2 年内未更新。2018 年，它被修改为依赖更相关的数据。由于 WordPress 每年发布 2 到 3 次主要更新，而一个维护中的插件应该与近期版本进行测试，因此当新版本的 WordPress 发布时，通过更新插件的 readme 文件可以避免此警报。

在每次 WordPress 主要版本发布前，开发者都会收到电子邮件，并被要求更新此值。他们_无需_发布新版本，只需[更新 readme 文件](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/)并将 `Tested up to:` 的值编辑为最新的 WordPress 版本即可。

# 区块特定插件指南

[info]所有区块特定插件还必须遵守[整体插件指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)。这些额外指南是区块特定插件独有的。[/info]

## 提交插件至区块目录指南

[区块目录](https://wordpress.org/plugins/browse/block/)的目标是为 WordPress 用户提供一个安全的空间，用于查找和安装新的区块。

### 用户期望

- 用户将能在 WordPress 管理后台和 WordPress.org 上找到可下载安装区块的专区，这些区块已通过重大安全问题的预先审查。
- 用户可在管理后台一键安装区块，每次安装一个。
- 区块在安装并激活后将显示在区块库中。
- 区块将无缝即时运行，不会出现侵扰性广告或升级推销。

### 开发者期望

- 开发者在为区块目录编写区块时，将拥有一套明确的需求和指南可供遵循。
- 遵循这些指南将有助于确保他们开发的区块能够无缝安装到编辑器中。
- 提交到区块目录的区块，相比一般的 WordPress 插件，需要遵守更严格的技术和政策规则。
- 包含不符合区块目录指南的区块的插件，仍可提交到插件目录。

### 定义

针对区块目录，我们区分两种类型的插件：

1. 仅用于分发区块的插件。
2. 所有其他插件，包括捆绑多个独立区块的插件、除区块外还包含其他功能的插件，以及完全不包含区块的插件。

这些指南专门适用于第一种类型，称为区块插件。如果插件属于第二种类型，则不适用进一步的指南和限制。所有插件，无论是否仅为区块插件，都必须遵守[详细插件指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)。

### 区块插件与区块目录

区块目录仅收录区块插件，即仅包含单个独立顶级区块及最少支持代码的插件。区块插件具有以下特征：

1. 属于特定类型的 WordPress 插件，具有相同结构（包含 `readme.txt` 文件）
2. 仅包含区块（通常为单个顶级区块）
3. 仅包含最少的服务端（如 PHP）代码
4. 自包含，在文章编辑器外无独立界面

除适用于所有 WordPress 插件的规范外，提交至区块目录的区块插件必须遵循以下准则：

### 指南

#### 1. 区块插件适用于区块编辑器。

一个区块插件必须包含一个区块，以及最少量的其他支持代码。它不能包含编辑器之外的任何用户体验，例如 WordPress 选项或 `wp-admin` 菜单。服务器端代码应保持在最低限度。

目前，仅扩展或为其他现有区块提供样式的插件不符合纳入区块目录的资格。目前，它们不受区块编辑器无缝安装过程的支持。目前仅支持注册新区块的区块插件。

#### 2. 区块插件是独立的区块。

区块插件旨在是单一用途、独立、分开的区块，而非多个区块的集合或汇编。在大多数情况下，一个区块插件应仅包含一个顶级区块。区块目录不会收录那些可以合理拆分为独立区块的区块集合。

当存在明确必要的父子或容器/内容依赖关系时，区块插件可以包含多个区块；例如，一个包含列表项区块的列表区块。

#### 3. 插件和区块名称应反映区块的用途。

插件标题和区块标题应描述区块的功能，以帮助用户轻松理解其用途。在大多数情况下，插件标题和区块标题应相同或非常相似。一些良好的插件和区块名称示例如下：

`Rainbow Block`
`Sepia Image Grid`
`Business Hours Block`

请避免使用与区块本身无关或难以与核心区块区分的插件和区块标题。一些无益的插件和区块名称示例如下：

`PluginCo Inc Block`
`Widget`
`Image Block`

区块标题和名称同样适用插件标题的商标限制。这意味着，除非由 Facerange 的合法代表开发，否则区块不得命名为“Facerange Block”。

#### 3a. 区块名称应唯一且正确命名空间化。

区块名称（指 [`registerBlockType()` 的 `name` 参数](https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#block-name) 和 [`block.json` 中的 `name`](https://github.com/WordPress/gutenberg/blob/master/docs/rfc/block-registration.md#name)）必须对该区块是唯一的。与标题一样，请尊重商标和其他项目常用的名称，以免产生冲突。

区块名称的命名空间前缀应反映插件作者或插件别名。例如：

`name: "my-rainbow-block-plugin/rainbow"`，或
`name: "john-doe/rainbow"`，或
`name: "pluginco/rainbow"`。

命名空间不能是保留的，例如 `core` 或 `wordpress`。

#### 4. 区块插件必须包含 `block.json` 文件。

区块注册 RFC 概述了 `block.json` 文件的格式：[https://github.com/WordPress/gutenberg/blob/master/docs/designers-developers/developers/block-api/block-metadata.md](https://github.com/WordPress/gutenberg/blob/master/docs/designers-developers/developers/block-api/block-metadata.md)

区块插件必须包含一个有效的 `block.json` 文件。除了 RFC 中规定的要求外，`block.json` 还必须包含以下属性：

- `name`
- `title`
- 至少包含一项：`script`、`editorScript`
- 至少包含一项：`style`、`editorStyle`

#### 5. 区块插件必须能独立工作。

区块插件必须能够独立运行，无需依赖任何外部组件，例如其他 WordPress 插件或主题。

区块插件可以使用其他 WordPress 插件或主题中的代码或钩子，但前提是该区块插件在未安装该插件或主题的情况下仍可使用。例如，一个食谱区块插件可以应用滑块插件中实现的过滤器来改善其图像显示，只要该食谱区块插件在没有滑块插件的情况下仍能正常使用。

#### 6. 区块插件应无缝运行。

区块插件从编辑器安装后，应能即时无缝运行。这意味着它们不应给用户带来额外步骤或前提条件，例如安装其他插件或主题、注册账户、登录或手动连接外部服务。

使用区块插件不允许任何形式的付费。虽然允许在插件的 readme 文件中使用捐赠链接功能，或从完整插件列表页面链接，但区块插件本身必须免费使用。确实需要付费服务或包含升级销售和高级功能的区块插件，只要符合其指导原则，仍可在 WordPress 插件主目录中发布。

区块插件可在必要时使用外部/云 API，前提是无需登录或激活密钥即可无缝完成。

它们不应依赖外部 API 或云服务来执行可在本地完成的功能。

#### 7. 应尽量减少服务器端代码。

由于区块插件是 WordPress 插件，它们必然包含用于元数据和初始化的 PHP 代码。它们应尽可能避免包含额外的 PHP 代码或服务器端库。WordPress REST API 应是首选的 WordPress 接口，而非自定义的服务器端代码。

REST API 存在限制，某些情况下服务器端 PHP 是实现功能的唯一高效方式。在这些情况下，可以包含 PHP 代码，前提是代码清晰、尽可能少用且文档完善。

#### 8. 不得包含广告或推广通知。

区块插件不得包含显示警告、仪表盘通知或与区块功能无关的类似干扰性消息的代码。

# 常见问题

本文档整理了插件审核团队在审查插件时遇到的一些最常见问题。

此列表包含团队邮件内容的摘录，不应被视为完整或详尽；审核结果取决于团队执行的人工审查。

## 安全

### 数据净化

**输入数据必须经过净化、验证，并在输出时进行转义**

当您在插件中使用 POST/GET/REQUEST/FILE 调用时，对它们进行净化、验证和转义非常重要。这样做的目的是防止用户意外通过系统发送垃圾数据，并保护他们免受潜在的安全问题。

**净化**：输入的数据（无论是用户输入还是自动生成）必须尽快进行净化。这可以减少 XSS 漏洞和 MITM 攻击的可能性，这些攻击可能篡改提交的数据。

**验证**：所有数据都应进行验证，无一例外。即使您进行了净化，也要记住，当唯一有效值是数字时，您不希望有人输入“dog”。

**转义**：输出的数据在回显时必须正确转义，以防止其劫持管理界面。WordPress 提供了许多 esc_*() 函数，您可以使用它们来确保不会向用户显示错误的数据。

为了帮助您完成这些操作，WordPress 内置了许多净化和转义函数。您可以在此处阅读相关内容：

https://developer.wordpress.org/apis/security/sanitizing/
https://developer.wordpress.org/apis/security/escaping/

请记住：您必须根据上下文使用最合适的函数。如果您要净化电子邮件，请使用 sanitize_email()；如果您要输出 HTML，请使用 wp_kses_post()，依此类推。

一个简单的口诀是：

**尽早净化**  
**最后转义**  
**始终验证**

净化一切，检查一切，转义一切，永远不要相信用户总是会输入合理的数据。毕竟，用户来自各行各业。

#### 清理：关于转义和清理函数的混淆

**注意**：转义函数不能用于清理。它们用途不同。即使它们看起来非常适合此目的，大多数函数都是可过滤的，人们期望用它们来转义。因此，另一个插件可能会改变它们的功能，使您的代码面临风险并可能被利用。

如果您尝试输出变量，必须先清理再转义，例如：

```php
echo esc_html(sanitize_text_field($_POST['example']));
```

#### 清理：使用过滤函数进行数据清理

**注意**：当使用 `filter_var`、`filter_var_array`、`filter_input` 和/或 `filter_input_array` 等函数时，你需要[将 FILTER 参数设置为任何能清理输入的过滤器类型](https://www.php.net/manual/en/filter.filters.php)。

如果过滤器参数留空，PHP 默认会应用过滤器 "FILTER_DEFAULT"，**它完全不进行清理**。

```php
$post_id = filter_input(INPUT_GET, 'post_id', FILTER_SANITIZE_NUMBER_INT);
```

#### 清理：随机数

**注意**：使用 `wp_verify_nonce` 检查随机数时，需要使用 `wp_unslash` **和** `sanitize_text_field` 清理输入，[这是因为该函数是可插拔的，扩展者不应信任其输入值](https://developer.wordpress.org/news/2023/08/understand-and-use-wordpress-nonces-properly/#verifying-the-nonce)。

示例：
```php
if ( ! isset( $_POST['prefix_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash ( $_POST['prefix_nonce'] ) ) , 'prefix_nonce' ) )
```

### 处理完整输入

我们强烈建议您**永远不要**尝试处理整个 $_POST/$_REQUEST/$_GET 堆栈。这会使您的插件变慢，因为您在不必要地遍历不需要的数据。相反，您应该只尝试处理插件运行所需的项目。

### 转义

**输出变量和选项时必须进行转义**

与全面清理数据密切相关，所有被输出的变量在输出时都需要进行转义，以防止其劫持用户界面或（更糟的）管理员界面。WordPress 提供了许多 esc_*() 函数，可确保您不会向用户显示错误数据，同时也有一些函数能让您安全地输出 HTML。

目前，我们要求您在**输出所有 $-变量、选项以及任何类型的生成数据时**进行转义。这意味着您不应在构建变量时转义，而应在最终输出时进行转义。我们称之为"延迟转义"。

除了保护自己免受潜在的 XSS 漏洞攻击外，延迟转义还能确保您为未来的自己提供安全保障。虽然当前您的代码可能只输出硬编码内容，但未来情况可能并非如此。通过在**输出时**花时间进行适当转义，您可以防止未来的错误演变为严重的安全问题。

对于已保存到数据库的选项，这一原则同样适用。即使您在保存时已进行适当清理，清理和转义的工具也不能互换使用。清理确保数据在处理和存储到数据库时是安全的，而转义则确保输出时的安全性。

还需注意，有时函数本应返回内容，却错误地进行了输出。这在返回 JSON 编码内容时是一个常见错误。实际上，您很少需要直接输出此类内容。输出是因为内容需要显示在屏幕上供人阅读，而返回（如 API 处理方式）可以进行 JSON 编码——但请记住在保存到 JSON 对象时要进行**清理**！

WordPress 提供了多种选项来保护各类内容（HTML、电子邮件等）。是的，即使是 HTML 也需要进行适当转义。

[https://developer.wordpress.org/apis/security/escaping/](https://developer.wordpress.org/apis/security/escaping/)

请记住：必须根据上下文使用最合适的函数。几乎每种可输出的内容都有对应的转义选项，即使是安全输出 HTML 也不例外。

#### 转义：esc_url_raw 的使用

我们知道这很令人困惑，`esc_url_raw` 函数并非转义函数，而是类似于 `sanitize_url` 的清理函数。具体来说，它用于清理 URL 以便在数据库或重定向中使用。

用于转义 URL 的合适函数是 `esc_url`。

#### 转义：__ 函数的使用

`__` 函数获取翻译文本时不进行转义，请选择以下任一方式：

- 使用能对结果值进行转义的其他函数，例如 `esc_html__` 或 `esc_attr__`。
- 或者用适当的转义函数包装 `__` 函数，例如 `esc_html`、`esc_attr`、`wp_kses_post` 等。

示例：
```php
<h2><?php echo esc_html__('Settings page', 'plugin-slug'); ?></h2>
<h2><?php echo esc_html(__('Settings page', 'plugin-slug')); ?></h2>
```

#### 转义：_e 和 _ex 的使用

函数 `_e` 和 `_ex` 会直接输出翻译内容而不进行转义，请使用能对输出进行转义的替代函数。

- `_e` 的替代方案可以是 `esc_html_e`、`esc_attr_e`，或者简单地在 `echo` 语句中使用 `__` 函数并用转义函数包裹。
- `_ex` 的替代方案可以是在 `echo` 语句中使用 `_x` 函数并用转义函数包裹。

示例：
```php
<h2><?php esc_html_e('Settings page', 'plugin-slug'); ?></h2>
<h2><?php echo esc_html(__('Settings page', 'plugin-slug')); ?></h2>
<h2><?php echo esc_html(_x('Settings page', 'Settings page title', 'plugin-slug')); ?></h2>
```

#### 转义：使用 json_encode

当你需要输出 JSON 时，最好使用 `wp_json_encode` 函数，同时确保在第二个参数中传递的选项不会绕过转义。

```php
echo wp_json_encode($array_or_object);
```

#### 转义：HTML

在转义时，有时您的插件需要输出 HTML。这可以使用函数 `wp_kses_post` 或 `wp_kses` 来完成。函数 `wp_kses_post` 将允许任何可以放在文章内容中的常见 HTML，`wp_kses` 将允许您使用其第二个和第三个参数设置的任何 HTML，请[参考其文档](https://developer.wordpress.org/reference/functions/wp_kses/)。

一个常见的错误是使用 `esc_html` 来转义 HTML。此函数并非用于此目的，它旨在转义将**放入** HTML 标签内的输出，因此它会去除任何 HTML 标签。

示例：
```php
echo wp_kses_post($html_content);
echo wp_kses($html_content, array( 'a', 'div', 'span' ));
```

#### 转义：使用净化函数

净化函数不能用于转义。它们用途不同。即使它们看起来非常适合此目的，但大多数函数是可过滤的，人们期望用它们来净化。因此，其他插件可能会改变它们的功能，使您的插件面临风险并可能被利用。

如果您要输出变量，必须先净化再转义，例如：

```php
echo esc_html(sanitize_text_field($_POST['example']));
```

### 文件

#### 文件：使用 WordPress 文件上传器

**请使用 WordPress 的文件上传器**

当插件使用 `move_uploaded_file()` 时，其上传的文件将绕过 WordPress 内置函数的检查与平衡机制。因此，您应当使用内置函数：

[https://developer.wordpress.org/reference/functions/wp_handle_upload/](https://developer.wordpress.org/reference/functions/wp_handle_upload/)

#### 文件：未过滤的上传

**不允许使用 ALLOW_UNFILTERED_UPLOADS。**

将此常量设置为 true 将允许用户上传任何类型的文件（包括 PHP 和其他可执行文件），从而造成严重的安全隐患。作为开发者，我们不应在任何逻辑中使用或允许使用此常量，即使在条件语句中也不行。

WordPress 包含一个安全文件列表，您可以在函数 [wp_get_mime_types](https://developer.wordpress.org/reference/functions/wp_get_mime_types) 中查看。

如果您需要添加列表中未包含且不会带来安全风险的特定文件，可以使用 [upload_mimes](https://developer.wordpress.org/reference/hooks/upload_mimes/) 过滤器来实现。

#### 文件：远程调用文件

禁止将图片、js、css 和其他脚本卸载到你的服务器或任何远程服务（如 Google、MaxCDN、jQuery.com 等）。当你调用远程数据时，会引入对另一个站点不必要的依赖。如果你调用的文件不是 WordPress 核心的一部分，那么你应该将其 -本地- 包含在你的插件中，而不是远程调用。如果该文件已包含在 WordPress 核心中，请直接调用它。

此规则的一个例外是，如果你的插件正在执行一项服务。我们将根据具体情况允许这样做。由于这可能令人困惑，我们提供了一些不被允许的示例：

- 将 jQuery CSS 文件卸载到 Google——你应该将 CSS 包含在你的插件中。
- 插入带有帮助文档的 iframe——首选链接或将文档包含在你的插件中。
- 从你自己的域名调用图片——它们应该包含在你的插件中。

以下是一些我们允许的示例：

- 从 Google 或其批准的 CDN 调用字体族（如果与 GPL 兼容）
- 向你的服务器进行 API 调用以处理可能的垃圾评论（如 Akismet）
- 将评论卸载到你自己的服务器（如 Disqus）
- 向服务提供商进行 oEmbed 调用（如 Twitter 或 YouTube）

请从你的插件中移除外部依赖，并尽可能将所有文件包含在插件内（而不是远程调用）。如果你认为你提供的是服务，请以解释服务、被调用的服务器以及是否需要账户连接的方式重写你的 readme.txt。

### 库

#### 库：使用开发版本

**使用库的 Beta / Alpha / 开发版本**

除非插件功能必需，否则我们不建议使用库的测试版本。您应当使用库最稳定的发布版本。

若因技术原因必须使用测试版本，请说明理由。否则，请将库更改为稳定版本。

#### 库：不再维护

**不再维护的库不允许使用**

我们不再接受使用任何开发者已停止支持或维护的库，因为它们会带来重大的安全风险。请考虑其他选项。

#### 库文件：已过时

您使用的至少一个第三方库已过时。请升级至最新稳定版本以获得更好的支持与安全性。我们不建议您使用测试版。

### WPDB：不安全的 SQL 调用

在进行数据库调用时，保护代码免受 SQL 注入漏洞的影响至关重要。您需要更新代码，在查询中使用 wpdb 调用和 prepare() 方法以增强安全性。

请查阅以下资料：

*   [https://developer.wordpress.org/reference/classes/wpdb/#protect-queries-against-sql-injection-attacks](https://developer.wordpress.org/reference/classes/wpdb/#protect-queries-against-sql-injection-attacks)
*   [https://codex.wordpress.org/Data\_Validation#Database](https://codex.wordpress.org/Data_Validation#Database)
*   [https://make.wordpress.org/core/2012/12/12/php-warning-missing-argument-2-for-wpdb-prepare/](https://make.wordpress.org/core/2012/12/12/php-warning-missing-argument-2-for-wpdb-prepare/)
*   [https://ottopress.com/2013/better-know-a-vulnerability-sql-injection/](https://ottopress.com/2013/better-know-a-vulnerability-sql-injection/)

#### WPDB：占位符数组

**注意**：使用占位符向 wpdb::prepare 传递单个值相当简单，但如果我们需要传递一个值数组呢？

你需要为数组中的每个项目创建一个占位符，并将所有对应的值传递给这些占位符，这看起来有些棘手，但这里有一个代码片段可以实现。

```php
$wordcamp_id_placeholders = implode( ', ', array_fill( 0, count( $wordcamp_ids ), '%d' ) );
$prepare_values = array_merge( array( $new_status ), $wordcamp_ids );
$wpdb->query( $wpdb->prepare( "
    UPDATE `$table_name`
    SET `post_status` = %s
    WHERE ID IN ( $wordcamp_id_placeholders )",
    $prepare_values
) );
```
有一个核心工单可能会在未来让这变得更容易：[https://core.trac.wordpress.org/ticket/54042](https://core.trac.wordpress.org/ticket/54042)

### 请勿使用 HEREDOC-NOWDOC

**请勿在您的插件中使用 HEREDOC 或 NOWDOC 语法**

虽然这两种语法完全有效，并且在许多方面是 PHP 中用于输出内容的理想特性，但对于大多数插件来说，其代价过高。

主要问题在于，当您使用 HEREDOC 或 NOWDOC 时，大多数（如果不是全部）代码检查工具将无法检测到代码中缺少转义的情况。尽管有解决方法，但最终结果会破坏所有可读性，留下一团混乱且无法被正确扫描的代码。

我们认为此处的风险远大于收益，因此不允许使用它们。

### 直接文件访问

**允许直接访问插件文件**

直接文件访问发生在有人直接查询 PHP 文件时。这可以通过在浏览器的 URL 栏中输入文件的完整路径，或直接向文件发送 POST 请求来实现。

对于仅包含类或函数定义的文件，直接访问时发生异常情况的风险很小。然而，对于包含可执行代码（例如，函数调用、类实例创建、类方法调用或包含其他 PHP 文件）的文件，安全问题的风险难以预测，因为它取决于具体情况，但风险可能存在且可能很高。

您可以通过在所有可能因直接访问而执行代码的 PHP 文件顶部添加以下代码来轻松防止这种情况：

```php
if ( ! defined( 'ABSPATH' ) ) exit; // 如果直接访问则退出
```

## 兼容性

### 前缀命名

**通用函数/类/定义/命名空间/选项名称**

所有插件必须具有唯一的函数名、命名空间、定义、类和选项名称。这能防止您的插件与其他插件或主题发生冲突。我们需要您更新插件，使用更独特且易于区分的名称。

一个很好的方法是使用前缀。例如，如果您的插件名为“Easy Custom Post Types”，那么可以使用以下名称：

- function ecpt_save_post()
- class ECPT_Admin{}
- namespace ECPT;
- update_option( 'ecpt_settings', $settings );
- define( 'ECPT_LICENSE', true );
- global $ecpt_options;

请不要再尝试使用两个（2）或三个（3）字母的前缀。仅在 WordPress.org 上我们就托管了近十万个插件，在我们服务器之外还有数万个。请相信我们，您会遇到冲突。

您还需要避免使用 __（双下划线）、wp_ 或 _（单下划线）作为前缀。这些是为 WordPress 本身保留的。您可以在类内部使用它们，但不能作为独立函数的前缀。

请记住，如果您使用 _n() 或 __() 进行翻译，这是可以的。我们**仅**讨论您为插件创建的函数，而不是 WordPress 的核心函数。事实上，正是因为这些核心功能，您才不应该在自己的插件中使用这些前缀！您不希望破坏用户的 WordPress 体验。

与此相关的是，在所有函数和类周围使用 if (!function_exists('NAME')) { 听起来是个好主意，直到您意识到致命的缺陷。如果其他代码有相同名称的函数并且其代码先加载，您的插件将会崩溃。if-exists 应仅用于共享库。

请记住：良好的前缀名称对您的插件来说是独特且易于区分的。这将有助于您和下一位开发人员进行调试，并防止冲突。

### PHP

#### PHP: Do not use Short Tags

The primary issue with PHP's short tags is that PHP managed to choose a tag (`<?`) that was used by another syntax: XML. This is a big issue when you consider how common XML parsing and management is.

We know as of PHP 5.4, `<?= ... ?>` tags are supported everywhere, regardless of short tags settings. This should mean they're safe to use in portable code but in reality that has proven not to be the case. Add on to that the fact that many codesniffers won't detect lack of escaping in code when you use short-tags, and it becomes not worth the headache for anyone.

Basically the risk here is way higher than the benefits, which is why we don't permit their use.

#### PHP：全局修改设置

**请勿强制全局设置 PHP 配置**

虽然许多插件可能需要 PHP 的最佳配置，但我们恳请您不要将其设为全局默认值。

在全局范围内运行类似 `ini_set('memory_limit', '-1');` 的定义（例如在 init 或代码的 `__construct()` 部分），意味着您将为网站上的所有内容运行此设置，这可能导致您的用户违反其主机上的任何限制或约束。

如果必须使用这些设置，您需要将其严格限制在仅确实需要的特定函数中。

#### PHP：设置默认时区

这通常不是一个好主意。用户应该能够在 WordPress 中定义自己的时区。

此外，WordPress 明确设置并期望默认时区为 UTC（在 settings.php 中），且日期/时间函数有时依赖于默认时区为 UTC 这一事实。例如，如果你执行 date_default_timezone_set(get_option('timezone_string'))，然后稍后尝试从 get_post_time() 或 get_post_modified_time() 获取 GMT 时间戳，它将无法给出正确的日期。

#### PHP：错误报告

**不要在生产代码中使用错误报告**

虽然 error_reporting() 是 PHP 中一个很棒的工具（[https://www.php.net/manual/en/function.error-reporting.php](https://www.php.net/manual/en/function.error-reporting.php)），但如果你在插件中永久设置它，就会给所有使用你代码的人带来麻烦。如果他们需要调试恰好使用了你代码的网站，他们将无法获得干净的测试结果，因为你在干扰输出。这在插件的日常功能中毫无用处。

### 插件标准

#### 主文件命名规范

**插件主文件的名称不符合规范。**

我们期望插件的主文件（包含插件头信息的文件）与插件文件夹同名，同时也应与插件的 slug / 固定链接同名。

例如，如果您的插件 slug 是 `ecpt-social-manager`，我们期望您的主插件文件名为 `ecpt-social-manager.php`。

请注意，使用某些常见名称作为主插件文件的文件名，在某些配置中可能会导致问题。

请查看我们关于如何[在插件中组织文件和文件夹](https://developer.wordpress.org/plugins/plugin-basics/best-practices/#folder-structure)的建议。

#### 不完整的文件头

您的插件文件头信息缺失或不完整。

请查阅[文件头要求](https://developer.wordpress.org/plugins/the-basics/header-requirements/)并相应更新您的插件，确保文件头信息仅存在于主文件中。

#### 不完整的自述文件

您的自述文件缺失或不完整。

在某些情况下，例如对于首个插件、有依赖项的插件或调用外部服务的插件，我们要求您提供完整的自述文件。这意味着您的自述文件必须包含标题、适当的描述以及关于其工作原理和使用方法的文档。

我们这样做的目标是确保每个人在安装前都清楚他们将要安装什么以及需要做什么。避免意外情况。如果您的插件正在调用其他服务器，这一点尤其重要。您需要在用户安装插件之前为他们提供所有必要的信息。

您的自述文件还必须通过 [Validator](https://wordpress.org/plugins/about/validator/) 验证，否则我们将拒绝它。请记住，我们不希望看到 readme.MD 文件。虽然它们可能有效，但 readme.txt 文件始终会被优先考虑，并且并非所有 Markdown 语法都能按预期工作。

我们请您基于此创建您的自述文件：[https://wordpress.org/plugins/readme.txt](https://wordpress.org/plugins/readme.txt)

#### 未声明 GPL 兼容许可证

必须声明此插件的许可证。您可以使用插件自述文件和插件头部中提供的字段来完成此操作。

请注意，所有代码、数据和图像——即存储在 WordPress.org 托管的插件目录中的任何内容——都必须符合 GPL 或 GPL 兼容许可证。包含的第三方库、代码、图像或其他内容也必须兼容。

有关兼容许可证的具体列表，[请阅读 gnu.org 上的 GPL 兼容许可证列表](https://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses)。

#### 错误的稳定标签

在您的 readme 文件中，'Stable Tag' 与主插件文件中指示的插件版本不匹配。

您的稳定标签应代表插件的稳定版本，而非 WordPress 版本。为了让您的插件能从 WordPress.org 正确下载，这些值必须保持一致。如果它们不同步，您的用户将无法获取正确的代码版本。

我们建议您使用语义化版本控制（即 SemVer）来管理版本：

- [软件版本控制](https://en.wikipedia.org/wiki/Software_versioning)
- [SemVer](https://semver.org/)

请注意：虽然目前在插件目录中使用 trunk 的稳定标签仍能工作，但这实际上并非受支持或推荐的指示新版本的方法，且已知会导致自动更新出现问题。

我们请您在发布新版本时正确使用标签并递增它们，就像更新主文件中的插件版本一样。保持两者匹配是实现完全向前兼容的最佳方式。

#### 声明的许可证不匹配

声明此插件的许可证时，必须保持一致。

请确保您在自述文件和插件头部声明的是相同的许可证。

此插件可以包含来自其他来源的代码，只要这些代码有完整文档记录且遵循 GPL 或 GPL 兼容许可证，但您的代码需要声明一个唯一的许可证。

### 使用 HTTP API

**使用 CURL 替代 HTTP API**

WordPress 自带一个功能全面的 HTTP API，应优先使用它而非自行创建 curl 调用。它既更快速又更全面。必要时它会回退到 curl，但会优先利用大量 WordPress 原生功能。

[https://developer.wordpress.org/plugins/http-api/](https://developer.wordpress.org/plugins/http-api/)

如有需要，您可以通过 [https://developer.wordpress.org/reference/hooks/http_api_curl/](https://developer.wordpress.org/reference/hooks/http_api_curl/) 使用 setopt。

请注意：如果您在第三方供应商库中使用 CURL，这是允许的。需要修正的是您插件专属代码（或任何专门的 WordPress 库）中的使用情况。

### 使用 wp_enqueue 命令

您的插件未能正确引入 JS 和/或 CSS。您应当使用内置函数来实现此功能：

当引入 **JavaScript 代码** 时，您可以使用：

- wp_register_script() 和 [wp_enqueue_script()](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) 来从文件添加 JavaScript 代码。
- [wp_add_inline_script()](https://developer.wordpress.org/reference/functions/wp_add_inline_script/) 来为先前声明的脚本添加内联 JavaScript 代码。

当引入 **CSS** 时，您可以使用：

- wp_register_style() 和 [wp_enqueue_style()](https://developer.wordpress.org/reference/functions/wp_enqueue_style/) 来从文件添加 CSS。
- [wp_add_inline_style()](https://developer.wordpress.org/reference/functions/wp_add_inline_style/) 来为先前声明的 CSS 添加内联 CSS。

请注意，自 WordPress 5.7 起，您可以通过新的函数和过滤器传递如 async、nonce 和 type 等属性：[WordPress 5.7 中的脚本属性相关函数](https://make.wordpress.org/core/2021/02/23/introducing-script-attributes-related-functions-in-wordpress-5-7/)

如果您尝试在管理页面中加载资源，您需要使用管理端的加载钩子。

- [admin_enqueue_scripts](https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/)
- [admin_print_scripts](https://developer.wordpress.org/reference/hooks/admin_print_scripts/)
- [admin_print_styles](https://developer.wordpress.org/reference/hooks/admin_print_styles/)

### 包含核心中已有的库

您的插件包含了一个 WordPress 已包含的库的副本。

WordPress 包含了许多有用的库，例如 jQuery、Atom Lib、SimplePie、PHPMailer、PHPass 等。出于安全性和稳定性考虑，插件不应在其自身代码中包含这些库，而必须使用 WordPress 打包的版本。

您可以在此处查看 JS 库列表：

[WordPress 包含并注册的默认脚本和 JS 库](https://developer.wordpress.org/reference/functions/wp_enqueue_script/#default-scripts-and-js-libraries-included-and-registered-by-wordpress)

虽然我们目前还没有一个像样的公开页面来列出所有这些库，但我们在此处有一个列表：

[核心贡献者](https://meta.trac.wordpress.org/browser/sites/trunk/api.wordpress.org/public_html/core/credits/wp-59.php#L739)

本地包含核心中没有的附加组件是可以的，但请仅添加那些额外的文件。例如，您不需要为了一个文件而包含整个 jQuery UI 库。如果您的代码与内置的 jQuery 版本不兼容，很可能是一个 noConflict 问题。

### 国际化

#### 国际化：使用变量

**国际化：请勿将变量或定义用作文本、上下文或文本域参数。**

为了让插件中的字符串可翻译，您需要使用一组特殊函数。这些函数统称为"gettext"。

WordPress 社区中有一个[专门的团队](https://make.wordpress.org/polyglots/)负责翻译并帮助其他译者将 WordPress 核心、插件和主题的字符串翻译成其他语言。

为了让翻译团队能够翻译此插件，请**不要在 gettext 函数的文本、上下文或文本域参数中使用变量或函数调用**，所有这些参数**都必须是字符串**。请注意，翻译解析器读取代码时不会执行它，因此无法读取这些函数中非字符串的内容。

例如，如果您的 gettext 函数如下所示...

`esc_html__( $greetings , 'plugin-slug' );`

...翻译者将看不到任何可翻译的内容，因为 `$greetings` 不是字符串，无法翻译。
您需要提供要翻译的字符串，这样他们才能在翻译系统中看到并翻译它，正确的写法如下...

`esc_html__( 'Hello, how are you?' , 'plugin-slug' );`

这也适用于翻译域，以下是一个错误的调用：

`esc_html__( 'Hello, how are you?' , $plugin_slug );`

正确的写法应该是：

`esc_html__( 'Hello, how are you?' , 'plugin-slug' );`

同时请注意，翻译域必须与您的插件别名相同。

如果要在翻译中包含动态值怎么办？很简单，您需要添加一个占位符作为字符串的一部分，并在 gettext 函数完成其工作后替换它，可以使用 `printf` 来实现，如下所示：

```php
printf(
    /* translators: %s: 用户的名字 */
    esc_html__( 'Hello %s, how are you?', 'plugin-slug' ),
    esc_html( $user_firstname )
);
```

您可以在此处[阅读更多信息](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#text-domains)。

## 合规性

### 更改插件激活状态

插件不允许更改其他插件的激活状态，此操作必须由用户执行。

同样不允许在用户激活或停用插件时进行干扰，除非是为了防止错误（例如：当您的插件依赖另一个插件时，若该插件未激活，则停用您自己的插件）。

WordPress 6.5 引入了[插件依赖](https://make.wordpress.org/core/2024/03/05/introducing-plugin-dependencies-in-wordpress-6-5/)，您可以使用它来管理依赖关系（尽管将其用作备用方案也是可以的）。

### 更新检查器

**包含更新检查器/更改更新功能**

请移除您插件中用于提供更新的检查代码。

我们不允许插件向其他服务器"拨号回家"检查更新，因为 WordPress.org 托管已为您提供此项服务。我们的指导原则之一是要求您实际使用我们的托管服务，因此需要您移除相关代码。

同时我们要求插件不要干扰内置更新器，否则在 WordPress 5.5 及以上版本中会导致用户遇到意外结果。

### 未记录的第三方服务

**未记录的第三方或外部服务使用**

我们允许插件要求使用第三方（即外部）服务，前提是这些服务已以清晰的方式妥善记录。

我们要求连接其他服务的插件以清晰易懂的语言披露此情况，以便用户了解数据被发送至何处。这使他们能够确保数据传输涉及的任何法律问题得到覆盖。即使您就是该第三方服务提供商，此要求同样适用。

为此，您必须更新您的自述文件以执行以下操作：

- 明确说明您的插件依赖第三方服务及其使用场景
- 提供该服务的链接
- 提供该服务的使用条款和/或隐私政策链接

请记住，这是为了您自身的法律保护。服务的使用必须公开透明并妥善记录。

### 包含非必需文件夹

此插件包含了一些看起来并非插件运行所必需的文件夹和文件。例如：

- 开发工具
- 生产环境中不需要的供应商文件夹（bower、node、grunt 等）
- 演示文件
- 单元测试

如果您试图包含自己代码的可读版本（以符合我们的指南），这是可以的，但请记住，我们也允许您在自述文件中提供这些文件的链接。

您还应保留和/或链接配置文件，例如 `composer.json` 文件，以便其他人可以审查、研究，甚至分叉此代码。

[详细插件指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#4-code-must-be-mostly-human-readable)

但是，您可以并且应该安全地从插件中删除那些其他非必需的文件夹。

### 不允许的文件

插件通常包含与功能相关的文件（php、js、css、txt、md），可能还有一些多媒体文件（png、svg、jpg）和/或数据文件（json、xml）。

我们检测到一些通常不会出现在插件中的文件，它们是必需的吗？如果不是，则这些文件将不被允许。

#### 包含“高级”来源的代码

某些高级库明确禁止被包含在免费（WordPress.org 托管的）插件中。这些代码必须被移除。

### GPL

#### GPL：包含非 GPL 兼容代码

所有代码必须兼容 GPLv2（或更高版本）许可证，才能被收录到我们的目录中。

您必须移除该代码并修改插件，使其不再依赖该代码。建议您寻找 GPL 兼容的代码作为替代。有关哪些类型的许可证与 GPL 兼容的更多信息，请查阅以下链接：

- [GNU 许可证 - 许可证列表](https://www.gnu.org/licenses/license-list.html)
- [GPL 常见问题 - 所有兼容性](https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility)

#### GPL：无公开文档资源

**您的压缩内容缺少公开文档资源**

在审核您的插件时，我们未能找到您 JavaScript 和/或 CSS 相关源代码的非编译版本。

为遵循我们关于代码需具备人类可读性的指导原则，我们要求您提供源代码和/或指向插件中所包含的非压缩开发者库的链接。如果提供链接，可将其置于源代码中，但我们也要求您在 readme 文件中注明。

[详细插件指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#4-code-must-be-mostly-human-readable)

我们坚信，开源的优势之一在于能够审查、观察和改编代码。通过维护一个公开的免费代码目录，我们鼓励并欢迎未来的开发者参与 WordPress 并推动其发展。

话虽如此，随着使用更复杂库的大型插件不断涌现，人们正充分利用构建工具（如 composer 或 npm）来生成分发的生产代码。为了在保持插件体积较小的同时仍鼓励开源开发，我们要求插件通过在 readme 中记录的方式，将任何压缩文件的源代码公开在易于查找的位置。

例如，如果您开发了一个 Gutenberg 插件并使用 npm 和 webpack 进行压缩和最小化，您必须在发布的插件中包含源代码，或提供一个可供审查、研究乃至分叉的公开维护源代码库。

我们强烈建议您包含关于使用任何构建工具的说明，以鼓励未来的开发者。

#### GPL：使用 composer 但缺少 composer.json 文件

我们注意到您的插件正在使用 Composer 管理库依赖，这非常棒，因为它将有助于未来维护和更新您的插件，同时避免与使用相同库的其他插件发生冲突。

`composer.json` 文件描述了您项目的依赖关系，也可能包含其他元数据。这是一个通常可以在插件最顶层目录中找到的小文件。

作为开源优势之一，代码的可审查、可观察和可适应性至关重要，**我们恳请您将此文件包含在您的插件中**，即使它仅用于开发目的。这将使他人能够行使我们共同受益的开源自由。

# 合规性免责声明

自 **2018年4月12日**起，[指南第9条](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#9-developers-and-their-plugins-must-not-do-anything-illegal-dishonest-or-morally-offensive)（开发者及其插件不得从事任何非法、不诚实或道德上令人反感的行为）已修订，新增了以下禁止事项：

- 暗示插件能够创建、提供、自动化或保证法律合规性

部分插件声称能帮助网站实现“合规”，例如符合ADA、GDPR、欧盟Cookie法、VAT、HIPPA、PCI等法规。

没有任何插件能提供100%的法律合规性。它们可以（也确实）帮助网站提高合规程度。但最终，确保网站满足任何合规要求的责任仍在于网站管理员。即使是专业服务也无法提供完全合规，仅在使用其服务时实现合规。

为了让网站管理员更清楚地理解这一点，我们建议插件**不要**声称能实现100%合规，而是说明插件本身将协助实现合规。这还有一个额外好处：当合规指南发生变化而插件尚未更新时，可以保护开发者。

## 我需要做什么？

简而言之：更新您的自述文件、文档、资源（横幅、截图等）和描述，以明确说明您的插件旨在**协助**实现合规性。

例如，如果您的插件声称“将使您的网站符合 ADA 标准”，您应将其更改为“将帮助您的网站更符合 ADA 标准”。此外，明智的做法是添加一条说明：“没有任何插件能提供 100% 的合规性”，然后列举您的插件为实现这一目标所做的努力。这也有助于您的销售宣传！

## 我需要更改插件显示名称吗？

如果您的插件名称声称或暗示合规性，那么是的。

例如，如果您的插件显示名称为“100% 欧盟 Cookie 法合规”或“VAT-MOSS 合规”，那么您应该将显示名称更改为“改进欧盟 Cookie 法合规”或“VAT-MOSS 合规助手”

请记住，一个更好的插件名称应该是您独有的。请记住，人们更容易找到“Foobar 的欧盟 Cookie 法工具”，而不是“欧盟 Cookie 法工具”——他们会更容易记住您的名字。

## 我的插件是一项服务且完全合规。我还需要这样做吗？

是的，但方式略有不同。

服务承担了这些情况下所需的合规责任，这需要在说明文档中明确说明：合规的不是插件代码，而是服务。我们强烈建议您链接到合规证明，并确保证明文件带有日期。

例如，不要说“Foobar 支付插件符合 PCI 标准”，而应说“Foobar 支付网关服务负责处理 PCI 合规事宜，但这仅适用于通过我们网关进行的交易。完整的合规详情可在我们的`[文档](https://service-name.com/doc/)`中找到。”

## 我的插件使用了声称 100% 兼容的第三方库，我需要更改吗？

除非你在自己的 readme/文档中也这样声明，否则不需要。不过你应该做个好人，提醒他们这样做并不准确。

## 我只讨论 100% 的 XHTML 合规性。我必须做出改变吗？

技术上不需要，但……你真的能永远保证 100% 合规吗？最好还是修改一下，只说“已更新以进一步提高 XHTML 合规性”。可以参考 WordPress.org 如何处理[无障碍合规性](https://wordpress.org/about/accessibility/)来获得一个好的起点。

## 如果我不这样做会怎样？

如果我们发现您做出不准确的声明，我们会发出警告。如果在合理时间内（60天）未修复，您的插件可能会被关闭。然而更严重的是，您可能会面临法律纠纷。虽然保护您免受此类风险并非我们的责任，但作为开源生态的守护者，我们认为有义务提醒您这些潜在问题。本质上，这可能对您（或您的公司）造成严重损害，而我们希望避免这种情况发生。

此外，从今往后，如果您的插件因其他问题（指南或安全相关）被关闭，我们将要求您在更新自述文件后才会重新开放插件。

## 如何举报其他插件违反此规定？

举报方式与常规流程相同。请发送邮件至 `plugins@wordpress.org`，附上相关插件链接并说明您认为其违规的理由。请注意，关闭插件是最后手段，我们可能已与相关开发者进行沟通。同时请记住，指出他人的错误并不能免除您自身的责任，您仍需修正自己插件的问题。

# 详细插件指南

[info]仅添加区块插件？请阅读[区块特定指南](https://developer.wordpress.org/plugins/wordpress-org/block-specific-plugin-guidelines/)。[/info]

## 插件目录

WordPress 插件目录旨在为所有 WordPress 用户——从非技术人员到开发者——提供一个安全的环境，下载符合 WordPress 项目目标的插件。

为此，我们希望确保开发者向目录提交插件的流程简单透明。作为我们持续努力使插件目录收录过程更加透明的一部分，我们制定了一份开发者指南。我们致力于为所有开发者创造一个公平的竞争环境。

如果您有改进指南的建议或相关问题，请发送邮件至 `plugins@wordpress.org` 告知我们。

## 开发者期望

所有拥有提交权限的开发者、所有官方支持插件的用户都应遵守以下准则：

- 插件目录准则（本文档）
- [社区准则](https://make.wordpress.org/handbook/community-code-of-conduct/)
- [论坛准则](https://wordpress.org/support/guidelines/)（若使用论坛/评价功能）

违规行为可能导致插件或插件数据（针对已批准的插件）从目录中移除，直至问题解决。根据违规性质及同行评审结果，用户评价和代码等插件数据可能无法恢复。重复违规可能导致作者所有插件被移除，并被禁止在 WordPress.org 托管插件。

插件开发者有责任确保其在 WordPress.org 的联系信息准确且及时更新，以便接收插件团队的所有通知。自动回复和转至支持系统的邮件不被允许，因其历来会阻碍人工及时处理邮件。

目录中的所有代码应尽可能确保安全。安全是插件开发者的最终责任，插件目录将尽力执行此要求。若发现插件存在安全问题，将被关闭直至问题解决。极端情况下，WordPress 安全团队可能为保护公众安全而更新插件并传播。

尽管我们尽可能考虑准则的各种相关解释，但期望涵盖所有具体情况是不合理的。若不确定插件是否违反准则，请联系 `plugins@wordpress.org` 咨询。

## 准则

### 1. 插件必须兼容 GNU 通用公共许可证

虽然任何 GPL 兼容许可证均可接受，但强烈建议使用与 WordPress 相同的许可证——“GPLv2 或更高版本”。所有代码、数据和图像——即存储在 WordPress.org 托管的插件目录中的任何内容——都必须遵守 GPL 或 GPL 兼容许可证。包含的第三方库、代码、图像或其他内容也必须兼容。有关兼容许可证的具体列表，请阅读 gnu.org 上的 [GPL 兼容许可证列表](https://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses)。

### 2. 开发者需对其插件的内容和行为负责

插件开发者有责任确保其插件内的所有文件符合规范。禁止故意编写代码规避规范，或恢复被要求删除的代码（参见第9条非法/不诚实行为）。

开发者在提交至SVN前，需确认所有包含文件的许可协议——从原始源代码到图像和库文件。此外，他们必须遵守其插件使用的所有第三方服务和API的使用条款。若无法验证库的许可协议或API的条款，则不得使用。

### 3. 插件必须在其 WordPress 插件目录页面提供稳定版本

WordPress.org 分发的唯一插件版本是目录中的版本。尽管开发者可能在别处编写代码，但用户将从目录下载，而非开发环境。

通过其他方式分发代码，却不保持此处托管的代码更新，可能导致插件被移除。

### 4. 代码必须（基本）具备可读性

目录中不允许使用类似 `p,a,c,k,e,r` 混淆功能、uglify 的 mangle 或诸如 `$z12sdf813d` 等不清晰的命名约定等技术或系统来隐藏代码以使其晦涩难懂。使代码不具备可读性会迫使未来的开发者面对不必要的障碍，同时也是隐藏恶意代码的常见载体。

我们要求开发者通过以下方式之一，公开且持续地提供对其源代码和任何构建工具的访问：

- 在已部署的插件中包含源代码。
- 在 readme 中提供指向开发位置的链接。

我们强烈建议您记录任何开发工具的使用方法。

### 5. 禁止试用软件

插件不得包含仅通过付费或升级才能解锁的受限或锁定功能。在试用期结束或达到配额后，功能不得被禁用。此外，仅提供沙盒访问 API 和服务的插件也属于试用或测试插件，同样不被允许。

服务中的付费功能是允许的（参见指南 6：服务软件），前提是插件内的所有代码完全可用。我们建议使用托管在 WordPress.org 之外的附加插件，以排除高级代码。仅作为开发者工具的插件将根据具体情况审查。

在指南 11（劫持管理体验）的范围内，尝试向用户推销临时产品和功能是可以接受的。

### 6. 允许软件即服务

允许作为某些外部第三方服务（例如视频托管网站）接口的插件，即使是付费服务也可接受。该服务本身必须提供实质性功能，并在随插件提交的自述文件中明确说明，最好附上服务使用条款的链接。

**不允许**的服务和功能包括：

- 仅用于验证许可证或密钥，而插件的所有功能方面都包含在本地的服务。
- 通过将任意代码移出插件来创建服务，以使该服务可能虚假地提供补充功能。
- 非服务的店面。仅作为从外部系统购买产品的前端的插件将不被接受。

### 7. 未经用户同意，插件不得追踪用户

为保护用户隐私，插件不得在未经用户明确授权的情况下联系外部服务器。通常通过“选择加入”方式实现，例如要求用户注册服务或在插件设置中勾选复选框。插件应在 readme 文件中说明如何收集和使用用户数据，最好附上明确的隐私政策。

禁止的追踪行为示例包括：

- 未经用户明确确认自动收集用户数据。
- 故意误导用户提交信息作为使用插件的前提条件。
- 加载与所提供服务无关的资源（包括图像和脚本）。
- 未记录（或记录不充分）的外部数据使用（例如黑名单）。
- 追踪使用情况和/或浏览量的第三方广告机制。

此政策的例外情况是软件即服务，例如 Twitter、Amazon CDN 插件或 Akismet。通过安装、激活、注册和配置使用这些服务的插件，即视为授予这些系统使用权限。

### 8. 插件不得通过第三方系统发送可执行代码

允许从有文档记录的服务外部加载代码，但所有通信必须尽可能安全。不允许在插件中执行外部代码（除非作为服务运行），例如：

- 从非 WordPress.org 的服务器提供更新或以其他方式安装插件、主题或附加组件。
- 安装同一插件的付费版本。
- 出于字体包含以外的原因调用第三方 CDN；所有与服务无关的 JavaScript 和 CSS 必须在本地包含。
- 在服务使用条款未明确允许的情况下，使用第三方服务管理定期更新的数据列表。
- 使用 iframe 连接管理页面；应使用 API 以最小化安全风险。

允许与网站交互并推送软件的管理服务，前提是该服务在其自身域内处理交互，而非在 WordPress 仪表盘中。

### 9. 开发者及其插件不得从事任何非法、不诚实或道德上令人反感的行为

尽管这一条款具有主观性且范围较广，但其目的在于防止插件、开发者和公司滥用最终用户及其他插件开发者的自由与权利。

这包括（但不限于）以下示例：

- 通过关键词堆砌、黑帽 SEO 或其他方式人为操纵搜索结果。
- 承诺为使用该插件的网站带来更多流量。
- 通过补偿、误导、施压、勒索或敲诈手段获取评论或支持。
- 暗示用户必须付费才能解锁已包含的功能。
- 创建账户以生成虚假评论或支持工单（即“马甲”行为）。
- 将其他开发者的插件据为己有并声称是原创作品。
- 暗示插件能够创建、提供、自动化或保证法律合规性。
- 未经许可利用用户的服务器或资源，例如将其用作僵尸网络或加密货币挖矿的一部分。
- 违反 [WordPress.org 社区行为准则](https://make.wordpress.org/handbook/community-code-of-conduct/)。
- 违反 [WordCamp 行为准则](https://make.wordpress.org/community/handbook/wordcamp-organizer/planning-details/code-of-conduct/)。
- 违反 [论坛指南](https://wordpress.org/support/guidelines/)。
- 对 WordPress 社区的任何其他成员进行骚扰、威胁或辱骂。
- 伪造个人信息以故意隐瞒身份，逃避先前违规行为的制裁。
- 故意试图利用指南中的漏洞。

### 10. 未经用户明确许可，插件不得在公开网站上嵌入外部链接或署名信息

插件代码中包含的所有“Powered By”或署名显示及链接必须为可选，且默认在用户面向公众的网站上 _不_ 显示。用户必须通过清晰明确、易于理解的选择来主动启用显示任何及所有署名和链接，这些选项不应隐藏在服务条款或文档中。插件不得以显示署名或链接作为其正常运行的前提条件。服务 _允许_ 按照其认为合适的方式对其输出进行品牌标识，前提是相关代码在服务端而非插件中处理。

### 11. 插件不应劫持管理仪表盘

用户更偏好并期望插件能融入 WordPress 的整体体验。持续不断的打扰和用不必要的提醒淹没管理仪表盘会破坏这种体验。

升级提示、通知、警报等必须限制范围并谨慎使用，无论是根据上下文还是仅在插件的设置页面。全站通知或内嵌的仪表盘小工具*必须*可关闭或在解决后自动消失。错误信息和警报必须包含如何解决问题的说明，并在完成后自行移除。

应避免在 WordPress 仪表盘中投放广告，因为这通常效果不佳。用户通常只在尝试解决问题时才会访问设置页面。让插件更难使用通常不会带来好评，我们建议限制其中放置的任何广告。请记住：通过此类广告进行推荐跟踪是不允许的（参见准则 7），且大多数第三方系统不允许后端广告。滥用广告系统的准则将导致开发者被向上游举报。

我们欢迎并鼓励开发者包含指向自己网站或社交网络的链接，以及在插件本地包含图像以增强体验。

### 12. WordPress.org 的公开页面（自述文件）不得包含垃圾信息

公开页面（包括自述文件和翻译文件）不得用于发布垃圾信息。垃圾信息行为包括（但不限于）不必要的联盟链接、竞争对手插件标签、使用超过 12 个标签、黑帽 SEO 和关键词堆砌。

允许适度包含指向直接必需产品（如插件使用所需的主题或其他插件）的链接。同样，相关产品可用于标签，但竞争对手产品不可。如果插件是 WooCommerce 扩展，可以使用 'woocommerce' 标签。但如果插件是 Akismet 的替代品，则不能使用该术语作为标签。重复使用标签或特定术语被视为关键词堆砌，是不允许的。

自述文件是为人而非机器人编写的。

在所有情况下，联盟链接必须明确披露，并且必须直接链接到联盟服务，而非重定向或隐藏 URL。

### 13. 插件必须使用 WordPress 的默认库

WordPress 包含许多有用的库，例如 jQuery、Atom Lib、SimplePie、PHPMailer、PHPass 等。出于安全性和稳定性考虑，插件不得在其自身代码中包含这些库。相反，插件必须使用 WordPress 打包的库版本。

有关 WordPress 中包含的所有 JavaScript 库的列表，请参阅 [WordPress 默认包含并注册的脚本](https://developer.wordpress.org/reference/functions/wp_enqueue_script/#notes)。

### 14. 应避免对插件进行频繁提交

SVN 仓库是发布仓库，而非开发仓库。所有提交（无论是代码还是 readme 文件）都会触发插件关联 zip 文件的重新生成，因此只有准备好部署的代码（无论是稳定版、测试版还是候选版）才应推送到 SVN。强烈建议每次提交都附带描述性强的信息性消息。频繁的“垃圾”提交消息（如“更新”或“清理”）会使他人难以跟踪变更。多次快速连续提交仅调整插件的微小方面（包括 readme），会给系统带来不必要的负担，并可能被视为操纵“最近更新”列表的行为。

唯一的例外是，当 readme 文件的更新仅用于表明支持最新版本的 WordPress 时。

### 15. 每个新版本必须增加插件版本号

只有当插件版本号增加时，用户才会收到更新提醒。主干 readme.txt 文件必须始终反映插件的当前版本。有关标记的更多信息，请阅读我们的 [SVN 标记说明](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/#tags) 和 [readme.txt 工作原理](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/)。

### 16. 提交时必须提供完整的插件

所有插件在审核通过前均需接受检查，因此必须提供压缩文件。名称不能为未来使用或品牌保护而"预留"（参见第17条：尊重品牌）。已批准但未使用的插件目录名称可能会分配给其他开发者。

### 17. 插件必须尊重商标、版权和项目名称

禁止将商标或其他项目名称作为插件标识符的唯一或起始部分，除非能提供合法所有权/代理权的证明。例如，[WordPress基金会已注册商标"WordPress"](https://wordpressfoundation.org/trademark-policy/)，在域名中使用"wordpress"属于侵权行为。此政策同样适用于插件标识符，我们不允许标识符以其他产品名称开头。

例如，仅Super Sandbox的员工应使用"super-sandbox"作为标识符，或在"Super Sandbox Dancing Sloths"这类语境中使用其品牌。非员工应采用"Dancing Sloths for Superbox"等格式，以避免可能误导用户认为该插件由Super Sandbox开发。同样，如果您不代表"MellowYellowSandbox.js"项目，则不应将其用作您的插件名称。

建议使用原创品牌，这不仅能避免混淆，也更容易被用户记住。

### 18. 我们保留尽最大努力维护插件目录的权利

我们的目标是以尽可能公平的方式执行这些准则。我们这样做是为了确保插件的整体质量及其用户的安全。为此，我们保留以下权利：

- ...随时更新这些准则。
- ...禁用或从目录中移除任何插件，即使原因未在准则中明确说明。
- ...给予例外情况，并允许开发者有时间解决问题，即使是与安全相关的问题。
- ...在出现新的、活跃的开发者时，移除原开发者对插件的访问权限。
- ...出于公共安全考虑，在未经开发者同意的情况下对插件进行更改。

作为回报，我们承诺将审慎使用这些权利，并尽可能尊重最终用户和开发者。

# 使用 Subversion

SVN（即 Subversion）是一种类似于 Git 的版本控制系统。您可以通过命令行或多种图形界面应用程序（如 [Tortoise SVN](https://tortoisesvn.net/)、[SmartSVN](https://www.smartsvn.com/) 等）来使用它。如果您是 SVN 新手，建议先查阅 [SVN 客户端对比](https://en.wikipedia.org/wiki/Comparison_of_Subversion_clients)再选择最适合您的工具。

本文档并非完整详尽的 SVN 使用指南，而是针对 WordPress.org 插件开发的快速入门指引。如需更全面的文档，请参阅 [《SVN 手册》](https://svnbook.red-bean.com/)。

这里我们将介绍与 WordPress.org 托管相关的 SVN 基础操作。SVN 及几乎所有代码仓库服务的基本概念都是相通的。

更多信息请查阅以下文档：

- [readme.txt 文件使用说明](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/)
- [插件资源文件（横幅图片与图标）使用指南](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/)

[warning]SVN 与插件目录是**发布**仓库。不同于 Git，您不应提交每个微小改动，否则可能影响性能。请仅将**已完成**的更改推送到 SVN 仓库。[/warning]

## 概述

您的所有文件都将集中存储在我们服务器上的 **SVN 代码库**中。任何人都可以从该代码库**检出**您的插件文件副本到本地计算机，但作为插件作者，只有您拥有**签入**权限。这意味着您可以在本地计算机上修改文件、添加新文件、删除文件，并将这些更改上传回中央服务器。正是通过这个签入过程，既更新了代码库中的文件，也更新了 WordPress.org 插件目录中显示的信息。

Subversion 会跟踪所有这些更改，以便您在需要时可以随时回溯查看旧版本或**修订版**。除了记录每个单独的修订版，您还可以指示 Subversion 为代码库的特定修订版**打标签**以便于引用。标签非常适合[标记插件的不同发布版本](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/#tags)，并且是确保 WordPress.org 上显示正确版本并为用户更新的唯一完全受支持的方法。

## 您的账户

您在 SVN 中的账户将使用您提交插件时所使用的账户用户名（而非邮箱）。这也是您在 WordPress 论坛中使用的用户 ID。

请注意，_大小写敏感_ —— 如果您的用户名是 JaneDoe，则必须使用大写的 J 和 D，否则 SVN 将无法识别。您可以在[个人资料页面](https://profiles.wordpress.org/me/profile/edit/group/1/)查看您用户名的具体大小写格式。

如需重置密码，请访问 [login.wordpress.org](https://login.wordpress.org/lostpassword)。

## SVN 目录

所有 SVN 仓库默认会创建三个目录。

```
/assets/
/tags/
/trunk/
```

- `assets` 用于存放[截图、插件头部信息和插件图标](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/)。
- 开发工作应放在 `trunk` 中。
- 发布版本应置于 `tags` 内。

_过去用于存放分支代码的 `/branches/` 目录因未被使用，现已不再默认创建。_

### 主干目录

[warning]请勿将你的 _主_ 插件文件放在 trunk 的子文件夹中，例如 `/trunk/my-plugin/my-plugin.php`，否则会破坏下载功能。你可以使用子文件夹来存放包含文件。[/warning]

`/trunk` 目录是你的插件代码应该存放的位置。主干目录可以被视为最新、最优秀的代码，但这不一定是最近的 _稳定_ 代码。主干目录用于开发版本。理想情况下，主干目录中的代码应该始终是可运行的，但由于它不一定是“稳定”版本，偶尔可能会存在错误。对于简单的插件，主干目录可能是代码的唯一版本，这也是完全可以接受的。

即使你在其他地方（如 git 仓库）进行开发工作，我们建议你保持 trunk 文件夹与你的代码同步更新，以便于进行 SVN 比较。

### 标签

`/tags` 目录用于存放插件的各个版本。此处的子目录版本号应与插件版本号保持一致。务必使用标签文件夹和正确的版本控制，以确保用户获取正确的代码。

插件 1.0 版本应位于 `/tags/1.0`，1.1 版本位于 `/tags/1.1`，依此类推。

我们**强烈**建议采用[语义化软件版本控制](https://semver.org/)。

### 资源文件

[info]另请参阅：[插件资源文件工作原理](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/)[/info]

资源文件目录存放您的截图、标题图像和插件图标。目录中部分旧版插件可能仍将截图文件置于 /trunk 目录，但这并非推荐做法。所有新插件都应将其截图置于 /assets 目录。这有助于保持插件文件体积小巧，因为无需随插件本身向 WordPress 安装环境发送截图文件。

### 分支

[alert]`/branches/` 目录默认不再创建，因为它基本未被使用。此部分可视为已弃用，仅作参考用途。[/alert]

`/branches` 目录可用于存储插件的分支。例如开发中的版本或测试代码等。

WordPress.org 系统**完全不会**使用分支目录，它严格被视为供开发者按需使用的空间。由于默认不再创建，若您不再需要，可忽略此目录。

## 最佳实践

为了让其他开发者更容易理解您的代码，以下实践被认为是最佳方案。

### 开发时请勿使用 SVN

这常常令人困惑。与 GitHub 不同，SVN 是一个 _发布_ 系统，而非开发系统。您无需提交和推送每一个微小的更改，实际上这样做对系统有害。每次将代码推送到 SVN 时，它都会为 SVN 中的所有版本重新构建 _所有_ 的 zip 文件。这就是为什么有时您的插件更新需要长达 6 小时才会显示。相反，您应该在准备就绪时一次性推送。

### 将代码存放于主干文件夹

许多人将 `trunk` 用作占位符。虽然可以直接更新 `trunk` 中的 `readme.txt` 文件并将所有内容放入标签文件夹，但这样做会使代码变更对比更加困难。相反，主干应包含代码的最新版本，即使该版本是测试版。

### 始终标记版本发布

虽然可以将主干分支用作插件的稳定标签，但这实际上不受支持也不推荐。相反，版本应正确标记并迭代发布。这将确保与任何自动更新器的完全兼容性，并在代码出现问题时允许回滚。

### 从主干创建标签

与其直接将代码推送到标签文件夹，你应该先在主干中编辑代码，并在 readme 中完善稳定版本信息，*然后*再将代码从主干复制到新标签。

这不仅便于查看任何更改，你还能提交更小的变更集，因为 SVN 只会更新已修改的代码。这将节省你的时间并减少潜在错误（例如更新到错误的稳定标签或将问题代码推送给用户）。

不必担心标签文件夹短暂不存在的情况。你可以使用 `svn cp` 将主干复制到标签，然后同时将它们推送到 SVN。

如果你在本地操作，则可以一次性更新主干并从中创建标签。检出仓库根目录，更新 /trunk 中的文件，然后执行 `svn copy /trunk /tags/1.2.3`（或任意版本号），最后一次性提交所有更改。SVN 是基于差异的系统，只要你使用 svn 执行复制操作，它就会保留历史记录，让他人轻松跟进。

### 删除旧版本

由于 SVN 是一个发布仓库，许多开发者选择删除其插件的旧版（不再支持的）版本。截至 2019 年，这不再能加速发布，因为构建过程仅处理已更改文件的标签。

## 示例

### 开始一个新插件

要启动你的插件，你需要将已有的文件添加到新的 SVN 仓库中。

首先在你的机器上创建一个本地目录，用于存放 SVN 仓库的副本：

```
mkdir my-local-dir
```

接下来，检出预构建的仓库

```
svn co https://plugins.svn.wordpress.org/your-plugin-name my-local-dir
> A my-local-dir/trunk
> A my-local-dir/branches
> A my-local-dir/tags
> Checked out revision 11325.
```

在我们的示例中，Subversion 已将中央 SVN 仓库中的所有目录添加（"A" 表示 "add"）到你的本地副本中。

要添加你的代码，请导航到 `my-local-dir` 文件夹：

```
cd my-local-dir
```

现在，你可以通过命令行的复制/粘贴命令，或拖放操作，将你的文件添加到仓库本地副本的 `trunk/` 目录中。使用你习惯的任何方式。

[warning]不要将你的 _主_ 插件文件放在 trunk 的子文件夹中，例如 `/trunk/my-plugin/my-plugin.php`，因为这会破坏下载。你可以为包含的文件使用子文件夹。[/warning]

一旦你的文件放入 trunk 文件夹，你必须让 Subversion 知道你想将这些新文件添加回中央仓库。

```
cd my-local-dir
svn add trunk/*
> A trunk/my-plugin.php
> A trunk/readme.txt
```

添加完所有文件后，你将把更改签入回中央仓库。

```
svn ci -m 'Adding first version of my plugin'
> Adding trunk/my-plugin.php
> Adding trunk/readme.txt
> Transmitting file data .
> Committed revision 11326.
```

所有签入都必须包含提交信息。

如果提交因“访问被禁止”而失败，并且你**确定**你有提交权限，请在签入命令中添加你的用户名和密码。

```
svn ci -m 'Adding first version of my plugin' --username your_username --password your_password
```

请记住，你的用户名是 _区分大小写_ 的。

### 编辑现有文件

当你的插件位于目录中后，很可能需要在某个时候编辑代码。

首先进入你的本地仓库副本，并确保它是最新的。

```
cd my-local-dir/
svn up
> At revision 11326.
```

在上面的例子中，我们完全是最新的。如果中央仓库有更改，它们会被下载并合并到你的本地副本中。

现在你可以使用你喜欢的任何编辑器来编辑需要更改的文件。

如果你没有使用 SVN 图形界面工具（如 SubVersion 或 Coda），你仍然可以在更改后检查并查看本地副本与中央仓库之间的差异。首先我们检查本地副本的状态：

```
svn stat
> M trunk/my-plugin.php
```

这告诉我们本地的 `trunk/my-plugin.php` 与我们从中央仓库下载的副本不同（"M" 表示 "已修改"）。

让我们看看该文件中具体发生了什么变化，以便我们可以检查并确保一切看起来正确。

```
svn diff
> * 输出的内容本质上是你的本地副本与你下载的原始副本之间
  * 执行标准 `diff -u` 命令的结果。
```

如果一切看起来都没问题，那么是时候将这些更改签入中央仓库了。

```
svn ci -m "fancy new feature: now you can foo *and* bar at the same time"
> Sending trunk/my-plugin.php
> Transmitting file data .
> Committed revision 11327.
```

现在你已经成功更新了主干。

### 为新版本“打标签”

每次正式发布插件时，都应该为该版本的代码副本打上标签。这能让您的用户轻松获取最新（或旧版）版本，让您更轻松地跟踪变更，并让 WordPress.org 插件目录知道应建议用户下载哪个版本的插件。

首先将代码复制到 `tags/` 目录的子目录中。为了适配 WordPress.org 插件浏览器，新的子目录应始终采用版本号格式。例如 `2.0.1.3` 是**正确**的，而 `Cool hotness tag` 是**错误**的。

我们希望使用 `svn cp` 而非普通的 `cp` 命令，以利用 SVN 的功能特性。

```
svn cp trunk tags/2.0
> A tags/2.0
```

与往常一样，提交变更。

```
svn ci -m "tagging version 2.0"
> Adding tags/2.0
> Adding tags/2.0/my-plugin.php
> Adding tags/2.0/readme.txt
> Committed revision 11328.
```

为新版本打标签时，**请务必更新** [`trunk/readme.txt`](https://wordpress.org/plugins/developers/#readme) 中的 `Stable Tag` 字段为新版本号。

恭喜！您已成功更新代码！

## 注意事项

切勿将任何你不愿或未准备好部署给所有插件用户的内容放入 SVN。这**包括**供应商文件、`.gitignore` 以及其他所有内容。

同时，请永远不要上传 zip 压缩包。与大多数代码仓库系统一样，SVN 要求你上传独立的文件。

### See Also

- [How the readme.txt works](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/).
- [How plugin assets (header images and icons) work](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/).
- [The SVN Book](https://svnbook.red-bean.com/).

# 插件自述文件

本页将解释插件目录的一些方面，并说明许多用户容易忽略的明显要点。

为使插件浏览器中的条目发挥最大效用，每个插件都应包含一个名为 `readme.txt` 的自述文件，且需符合 [WordPress 插件自述文件标准](https://wordpress.org/plugins/readme.txt)。该文件控制目录前端页面的显示内容。在自述文件中编写的描述将直接决定 `wordpress.org/plugins/Your-Plugin` 页面的显示内容。

您可以使用 [插件自述文件生成器](https://generatewp.com/plugin-readme/) 创建内容，并通过 [官方自述文件验证器](https://wordpress.org/plugins/developers/readme-validator/) 检查结果。如需更直观的辅助工具，可使用 [wpreadme.com](https://wpreadme.com/)。

自 WordPress 5.8 起，系统[不再解析自述文件中的运行要求](https://core.trac.wordpress.org/ticket/48520)。这意味着 `Requires PHP` 和 `Requires at least` 标头信息将改由插件主 PHP 文件解析。

## 章节详情

所有插件都包含一个主 PHP 文件，并且几乎所有插件都有一个 `readme.txt` 文件。`readme.txt` 文件应使用 Markdown 的子集来编写。

### 插件说明文件头部信息

插件说明文件的头部包含以下信息：

```
=== 插件名称 ===
贡献者: (此处应为 WordPress.org 用户 ID 列表)
捐赠链接: https://example.com/
标签: tag1, tag2
最低 WordPress 版本要求: 4.7
测试至: 5.4
稳定版本: 4.3
最低 PHP 版本要求: 7.0
许可证: GPLv2 或更高版本
许可证链接: https://www.gnu.org/licenses/gpl-2.0.html
这里是插件的简短描述。不应超过 150 个字符。此处不使用任何标记语言。
```

- **贡献者** – 一个区分大小写、以逗号分隔的列表，包含所有为代码做出贡献的 WordPress.org 用户名。通常，包含分叉项目贡献者的名字被视为一种尊重。有些开发者会要求从列表中移除他们的名字，因为他们不希望其他插件出现在他们的个人资料页面上。最好尊重这些请求。请记住，*仅*使用 WordPress.org 用户名 – 其他任何内容都将显示为无个人资料链接和 Gravatar 头像。要更改某人的显示名称（显示在插件面向用户的页面上），请编辑个人资料 `https://wordpress.org/support/users/YOURID/edit/` 并更改显示名称。
- **捐赠链接** – (可选) 在侧边栏生成“捐赠给此插件”链接。如果没有链接，则不显示任何内容。
- **标签** – 1 到 5 个以逗号分隔的术语，用于描述插件。插件必须避免使用竞争对手的插件名称作为标签。插件不应使用该插件独有的标签，因为这些标签将不会显示。
- **测试至** – 插件已测试兼容的 WordPress 版本。此字段*忽略*次要版本，因为插件不应因次要更新而失效。这意味着插件只需定义其测试过的主要版本，WordPress.org 插件目录将自动添加次要版本。此处应*仅*使用数字，例如‘4.9’而非‘WP 4.9’。
- **最低 PHP 版本要求** – (可选) 使用此插件所需的 PHP *最低*版本。此处应*仅*使用数字，例如‘7.0’而非‘PHP 7.0’。
- **稳定版本** – 插件的稳定版本。这*不是* WordPress 的版本，而是插件本身的版本。仅使用数字和句点，推荐使用[语义化版本格式](https://semver.org/)。
- **许可证** – 插件使用的 GPLV2（或更高版本）兼容许可证。
- **许可证链接** – (可选) 许可证的链接。这是可选的，但如果插件使用较罕见的许可证，则强烈推荐提供。

在头部部分的末尾是用于插件 _简短_ 描述的位置。示例建议不超过 150 个字符且不使用标记。该行文本是插件的单行描述，会直接显示在插件名称下方。如果超过 150 个字符，它会被截断，因此请保持简短。

### 安装

如果您的插件没有自定义安装设置，可以省略此部分。如果插件安装后需要特殊配置说明，此处是放置相关信息的理想位置。

### 自定义区块

虽然允许并支持使用自定义区块，但请适度使用。用户已习惯其他插件的呈现方式，若您的插件样式过于特殊，可能导致用户遗漏重要信息。

## 技术细节

虽然自述文件的大部分内容不言自明，但有几个部分容易让人困惑。

### 自述文件的解析方式

WordPress.org 插件目录的运行基于自述文件中 **Stable Tag** 字段的信息。当 WordPress.org 解析 `readme.txt` 时，它首先查看 `/trunk` 目录中的 `readme.txt`，读取其中的 "Stable Tag" 行。

当 Stable Tag 正确设置时，WordPress.org 会去 `/tags/` 目录中查找引用的版本。因此，Stable Tag 设为 "1.2.3" 将使其查找 `/tags/1.2.3/`。

[tip]标签文件夹中的 readme.txt 也必须正确更新以包含正确的 "Stable Tag" —— 否则可能导致您的插件无法更新。[/tip]

如果 Stable Tag 是 1.2.3 且 `/tags/1.2.3/` 存在，那么系统将不再读取 trunk 中的任何内容进行解析。如果您尝试更改 `/trunk/readme.txt` 中的插件描述，您的更改将不会在插件页面上生效。所有内容都来自 Stable Tag 指向的文件中的 `readme.txt`。

WordPress.org 插件目录会读取主插件 PHP 文件以获取插件名称、插件 URI 以及最重要的版本号等信息。在插件页面上，您会看到下载按钮显示 "下载版本 1.2.3" 或类似内容。该版本号来自插件的主 PHP 文件，_而非_ 自述文件！

Stable Tag 指向 `/tags` 目录中的一个子目录。但插件的版本实际上并非由该文件夹名称决定。相反，插件 PHP 文件中列出的版本决定了名称。如果您将 Stable Tag 更改为 1.4，而 PHP 文件中插件版本仍为 1.3，则列出的版本将是 1.3。

[warning]虽然在插件目录中仍可使用 **stable tag** 设为 `trunk`（而非版本号），但这既不受支持也不推荐作为指示新版本的方法，并且已知会导致自动更新问题。我们目前积极反对使用 "Stable Tag: trunk"，并禁止新插件使用此设置。[/warning]

### 视频

您可以在文档中嵌入来自 YouTube、Vimeo 以及任何其他 [WordPress 默认支持](https://wordpress.org/support/article/embeds/)平台的视频。您只需将视频 URL 单独粘贴到 readme 文件的一行中即可。

我们建议您**不要**将视频放在 FAQ 部分的最后一行，因为有时格式会变得很奇怪。

### Markdown

这些自述文件使用定制版的 [Markdown](https://daringfireball.net/projects/markdown/)。大多数 Markdown 语法都能按预期工作。

Markdown 也允许在 readme.txt 中轻松添加链接。只需这样书写即可将词语链接到 URL：

\[WordPress\](http://wordpress.org)

视频也可以放入 readme.txt 中。单独一行的 YouTube 或 Vimeo 链接会自动嵌入。还可以使用 wpvideo 短代码嵌入托管在 VideoPress 上的视频。

### 字段详情

对于想要确切了解解析对应关系的人员：

- **作者**
  插件头部的作者字段和自述文件中的贡献者字段。
- **版本**
  插件头部的版本字段。
- **标签**（即分类）
  自述文件中的标签字段。
- **插件名称**
  自述文件中的插件名称，若缺失则回退到插件头部指定的插件名称。
- **作者与插件主页**
  插件头部的作者 URI 和插件 URI 字段。插件 URI 应对每个插件保持**唯一**。请**勿**为免费版和付费版插件使用相同的 URI，否则会导致不良后果。
- **最后更新时间**
  版本号变更后，在相应目录中最后一次提交的时间。
- **创建时间**
  **首次**提交的时间。

### 文件大小

虽然自述文件是简单的文本文件，但文件大小超过 10k 可能会导致错误。您的自述文件应简洁明了。描述不应是推销辞令，而应侧重于说明插件功能、用途及使用方法。安装指南应直接明了。常见问题解答应切实解决问题。

至于更新日志，我们建议将当前版本信息保留在自述文件中，其余内容可拆分至独立文件——例如 `changelog.txt`。通过将历史更新日志存储在该文件中，既能保持自述文件精简，又便于需要详细更新记录的用户自行查阅。

同理，若需包含内嵌图片等深度文档，建议引导用户访问您的独立网站。

## 另请参阅

- [插件头部信息](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/)（位于插件的主文件中）

# 插件规划、提交与维护

您已经编写了下一个 [Hello Dolly](https://wordpress.org/plugins/hello-dolly/)，并希望全世界都能使用它。接下来该怎么做？

## 1. 反复测试

您的插件很可能会被许多用户在各种不同场景和托管环境中使用。您需要确保已充分测试插件，保证其在任何情况下都能正常运行，避免给用户带来困扰。

## 2. 选择一个好名字

插件名称应体现您和您作品的独特性。选择名称时，请确保不侵犯商标权或与他人产品名称冲突。例如，如果您并非为 FaceRange（一家虚构公司）工作，就不应将插件命名为“FaceRange 的跳舞松鼠”。更好的名称可以是“FaceRange 跳舞松鼠插件”。构思一个好名字可能很困难，请耐心斟酌。提交后插件 URL 无法更改，但显示名称可随时调整。

显示名称基于主插件文件的头部信息生成，因此请务必仔细检查。

## 3. 撰写优质文档

[README.txt](https://wordpress.org/plugins/developers/#readme) 文件是最好的起点，因为它是所有插件的标准参考点。你需要确保包含以下内容：

- 简明描述你的插件实际功能。如果功能过多，或许拆分成两个插件更好。
- 安装说明，特别是需要特殊配置时。如果用户需要注册你的服务，务必提供链接。
- 获取支持的指引，以及你提供和不提供哪些支持。

## 4. 提交你的插件

要提交插件，需要完成以下三个步骤：

1. 在 WordPress.org 上注册一个有效且定期检查的邮箱地址。如果你代表公司提交插件，请使用公司的**官方**邮箱进行验证。
2. 在你的邮件客户端中将 `plugins@wordpress.org` 加入白名单，以确保能收到邮件通知。
3. [提交你的插件](https://wordpress.org/plugins/developers/add/)，并附上简要的功能概述以及一个完整、可直接使用的插件 zip 包。该 zip 包必须是插件的完整版本，就像你通过插件安装器手动上传时一样。

插件进入审核队列后，我们将在 14 个工作日内审查代码是否存在问题。遵循指南可以避免大部分问题。如果发现问题，我们会联系开发者并共同寻求解决方案。审核通过后，你将收到一封邮件，其中包含如何访问[Subversion 仓库](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/)的详细信息，你将在该仓库中存储你的插件。

当你通过 SVN 在该仓库中上传插件（以及[readme 文件](https://wordpress.org/plugins/developers/#readme)）后，它就会出现在[插件目录](https://wordpress.org/plugins/)中。

## 5. 发布首个版本

WordPress.org 插件目录是潜在用户下载和安装插件的最便捷途径。WordPress 与插件目录的集成意味着用户只需点击几下即可更新您的插件。

当您准备好发布第一个版本时，您需要[注册](https://wordpress.org/plugins/developers/add/)。审核流程成功完成后，您将获得一个用于存放代码的 Subversion 仓库。我们提供了关于[在 WordPress.org 上使用 SVN](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/) 的文档，如果您习惯使用 GIT，其工作流程可能略有不同。

## 6. 拥抱开源

开源是我们这个时代最强大的理念之一，因为它促进了跨越国界的协作。通过鼓励贡献，你让他人也能像你一样热爱你的代码。开源你的代码有几种选择：

- [Github](https://github.com/) 让他人轻松参与你的项目。其他开发者和用户可以方便地提交错误修复或报告、功能请求或全新的贡献。Github 有一个[优秀的文档门户](https://support.github.com/)。
	- [Bitbucket](https://bitbucket.org/) 和 [Gitlab](https://about.gitlab.com/) 是具有类似功能的替代方案。
- WordPress.org 插件目录提供并要求你使用 [Subversion 仓库](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/)。

## 7. 倾听用户的声音

你常会发现，用户对你的代码进行了远超你想象的多种测试。这是极其宝贵的反馈。

通过 WordPress.org 发布代码意味着你的插件会自动拥有一个支持论坛。善用它！你可以订阅邮件接收新帖子，并及时回应用户。他们只是希望像你一样热爱你的插件。

Jetpack 有一篇关于[如何撰写优质错误报告](https://jetpack.com/blog/how-to-write-a-great-bug-report/)的文章可供参考。

## 8. 定期推送新版本

优秀的插件会持续迭代，不断推出小幅更新。不要因等待过久而让心血之作变得陈旧。请注意，频繁升级可能导致“更新疲劳”，用户会停止更新。在更新过少与过多之间保持平衡至关重要。

## 9. 反复迭代

如同生活的其他方面，最好的成果源于耐心与努力。

# Index

`assets` 文件夹是您插件中用于存放插件展示页所用图像（如插件横幅、图标和截图）的位置。

所有文件都应放置在 SVN 目录的 `assets` 文件夹中，该文件夹适用于插件的所有版本。这是一个顶级目录，与 `trunk` 同级。请**不要**将截图放入 `trunk/assets` 或 `tags/1.0/assets` 中。

所有图像均通过 CDN 提供并经过深度缓存，因此更改或添加后可能需要一些时间才能更新。请在报告图像无法显示前，给予代理服务器一定时间获取并缓存您的图像。通常只需几分钟，但在服务器高负载时（例如 WordPress 主要版本发布前一周及发布期间）可能需要长达 6 小时。

[默认图像尺寸](#default-image-sizes)
-------------------------------------------

图像尺寸应与文件名所暗示的尺寸一致。例如，`banner-772x250.png` 应为 772 像素宽、250 像素高。同样，`icon-256x256.png` 应为 256×256 的正方形。

我们**没有**定义任何新的横幅尺寸，因此请不要自作聪明地重命名您的图像并期望它们能正常显示。它们将无法显示。同样，请勿将图像制作得更大（或更小）却使用现有名称，否则显示效果会很糟糕。

[插件横幅](#plugin-headers)
---------------------------------

插件横幅是您在插件页面顶部看到的图像：

[![](https://i0.wp.com/developer.wordpress.org/files/2015/05/Screenshot-2024-07-02-at-12.50.04%E2%80%AFPM.png?resize=1024%2C472&ssl=1)](https://i0.wp.com/developer.wordpress.org/files/2015/05/Screenshot-2024-07-02-at-12.50.04%E2%80%AFPM.png?ssl=1)

设计横幅图像时，请考虑国际插件目录的使用情况。其中一些目录（如[希伯来语](https://he.wordpress.org/plugins/)和[阿拉伯语](https://ar.wordpress.org/plugins/)）使用从右到左（RTL）的语言。理想情况下，应设计横幅使其包含的元素可以从右到左或从左到右排列。您可以为 RTL 页面创建不同的图像，在文件名中加入 `-rtl`。

### [横幅文件名](#banner-filenames)

* 普通横幅：`banner-772x250.(jpg|png)`
* 本地化普通横幅：`banner-772x250-(rtl|es|es_ES).(jpg|png)`
* 高DPI（视网膜）横幅：`banner-1544x500.(jpg|png)`
* 本地化高DPI（视网膜）横幅：`banner-1544x500-(rtl|es|es_ES).(jpg|png)`

图像可以针对特定语言或所有从右向左（RTL）语言进行本地化。  
区域设置可以指定为完整区域设置（`es_ES`）或部分区域设置（`es`）。如果语言是RTL且未提供特定区域设置的图像，则会检查`rtl`图像。  
请勿在不做修改的情况下将英文图像复制到RTL和特定区域设置文件中，否则将使用英文变体。

关于RTL图像的示例，请查看bbPress的[英文版](https://wordpress.org/plugins/bbpress/)和[阿拉伯语版](https://ar.wordpress.org/plugins/bbpress/)。

关于视网膜图像的示例，请[查看Hello Dolly](https://wordpress.org/extend/plugins/hello-dolly/)或[Pluginception](https://wordpress.org/plugins/pluginception/)。您_不能_单独使用视网膜图像，它仅作为772×250图像的“附加”功能。较大的“视网膜”图像仅用于能够显示更高细节的显示器。

横幅图像的最大尺寸为4MB，但越小越好。

[插件图标](#plugin-icons)
-----------------------------

插件图标是方形图像，在WordPress.org的搜索结果中以及WordPress.org的后台显示在插件旁边。

[![带有其插件图标的Akismet](https://i0.wp.com/developer.wordpress.org/files/2015/05/akismet1.png?resize=651%2C235&ssl=1)](https://i0.wp.com/developer.wordpress.org/files/2015/05/akismet1.png?ssl=1)

除了JPG和PNG格式外，您还可以使用**SVG**。矢量图非常适合图标，因为它们可以缩放到任何尺寸，且文件本身很小。如果您选择使用SVG，则_必须_同时使用PNG图标作为后备，否则您的插件图标在旧版浏览器或Facebook上可能无法正确显示。

如果您不使用图标，系统将为您自动生成一个。示例如下图的圆形图标：

[![自动生成图标示例](https://i0.wp.com/developer.wordpress.org/files/2015/05/auto-generated-icons.jpg?resize=798%2C332&ssl=1)](https://i0.wp.com/developer.wordpress.org/files/2015/05/auto-generated-icons.jpg?ssl=1)

图标的最大文件大小为1MB，但与横幅一样，越小越好。

### [文件名](#filenames)

* 标准：`icon-128x128.(png|jpg|gif)`
* 高分辨率（Retina）：`icon-256x256.(png|jpg|gif)`
* SVG：`icon.svg`

目前没有更改这些尺寸的计划。

[截图](#screenshots)
-------------------

截图会显示在插件的主页上，用于展示插件管理仪表板或实际示例。您的 `readme.txt` 文件中的每一行都应对应**一张**截图。这些行的内容将成为插件页面上截图的说明文字。

例如：`1. 这是一只猴子`

这将在第一张截图下方显示说明文字“这是一只猴子”。推测是关于猴子的截图。

截图**必须**是本地文件才能显示。指向外部文件的链接将无法使用。

截图的最大文件大小为 10MB，但一如既往，文件越小越好。

### [文件名](#filenames-2)

* `screenshot-1.(png|jpg)`
* `screenshot-2.(png|jpg)`

所有文件名应为小写；大写名称将无法生效。

截图可以[类似横幅](#banner-filenames)进行本地化，当插件在德语环境下查看时，以下文件名将优先于上述英文名称：

* `screenshot-1-de.(png|jpg)`
* `screenshot-2-de.(png|jpg)`

[问题](#issues)
-----------------

如果您发现用户从 WordPress.org 插件目录页面点击您的图片时，图片会直接下载，您需要通过 SVN 更改上传方式。这是由于某些图片以 `Content-Type application/octet-stream` 格式发送所致。

要修复此问题，请运行以下命令：

    svn propset svn:mime-type image/png *.png
    svn propset svn:mime-type image/jpeg *.jpg

或者，插件作者可以在其 ~/.subversion/config 文件中设置：

    *.png = svn:mime-type=image/png
    *.jpg = svn:mime-type=image/jpeg

但这仅适用于新文件。修复已提交的文件仍需使用上述命令。

# 插件开发者常见问题解答

托管 WordPress 插件涉及诸多细节。在寻求帮助前，请花点时间查看您的问题是否已在此得到解答。

[info]最后更新：2024 年 8 月 25 日[/info]

## 插件团队

### 如何联系插件团队？

您可以通过电子邮件 `plugins@wordpress.org` 联系我们——我们会在 7 个工作日内回复所有邮件。

### 我可以加入团队吗？

请查看[此手册页面](https://make.wordpress.org/plugins/handbook/apply/)。

## 提交与审核

### 在哪里提交我的插件？

请前往[添加](https://wordpress.org/plugins/developers/add/)页面并上传您的文件。您应确保：

- 文件为 .zip 格式且大小不超过 10Mb。
- 采用常见的 WordPress 插件格式，以便能通过 WordPress 的“上传插件”功能安装。
- 已准备就绪：完整、无错误、无多余日志、无开发工具，且所有必要文件均已正确编译或生成。

我们不接受占位符或尚未准备就绪的插件。

### 如果我的插件超过 10 兆怎么办？

请仔细检查是否包含了未使用的文件（如测试文件夹、文档以及完整的 node/vendor 文件夹）。大多数遇到此问题的插件都包含了各种不应出现在最终代码中的开发内容。

### 提交后会发生什么？

您将立即收到一封自动邮件，告知提交情况。您的插件将进入队列，一旦我们开始处理，便会手动下载并审核代码。如果安全性、文档或展示方面没有问题，您的插件将被批准。如果发现问题，您将收到第二封邮件，详细说明需要修复的内容。

### 我的插件固定链接（slug）会是什么？

提交插件后，您将收到一封自动邮件，告知您插件 slug 的命名。该名称基于您主插件文件（包含插件头部信息）中的“插件名称”字段自动生成。

例如：

- 插件名称：`Boaty McBoatface`
- 自动生成的 Slug：`boaty-mcboatface`

如果已有同名或同 slug 的插件存在，[提交时您会收到警告](#为什么我的提交失败并提示插件名称已存在)。

该 slug 还将决定以下内容：

- 插件在 WordPress.org 公开页面的 URL：`wordpress.org/plugins/boaty-mcboatface`
- 插件在 WordPress 插件目录中的文件夹名称：`<wp-content-folder>/plugins/boaty-mcboatface`
- 插件的 SVN 仓库和 trac 地址：`plugins.svn.wordpress.org/boaty-mcboatface` 和 `plugins.trac.wordpress.org/browser/boaty-mcboatface`
- 插件国际化函数使用的文本域：`esc_html__('Hello', 'boaty-mcboatface');`

插件一经批准，此名称**无法**更改。请谨慎选择。

### 为什么我得到的短链接与告知的不同？

如果我们必须更改您的固定链接（短链接），我们总会通过电子邮件向您解释原因。通常，我们会在您有明显拼写错误或错误（例如将 _founder_ 写成 _foundre_）或与现有商标或其他插件冲突时更改您的固定链接。请务必仔细阅读您的审核邮件，因为我们确实会解释我们这样做的原因。

### 为什么我的提交失败，提示插件名称已存在？
出现这种情况有两个原因：

1.  您尝试使用的插件固定链接已在 WordPress.org 上存在
2.  您尝试使用的插件固定链接在 **WordPress.org 之外** 存在且拥有大量用户群

第一种情况显而易见。您不能拥有两个相同固定链接的插件，因此需要选择一个新的。

第二种情况令人困惑，因为它提示您该固定链接在 WordPress.org 之外的某个地方已被使用。需要理解的是，插件更新 API 的工作方式是将插件文件夹名称（即固定链接）与 WordPress.org 上托管的每个插件进行比较。如果匹配，则会检查更新并提示用户升级。

当这种情况发生时，“原始”插件（我们未托管的那个）的用户将升级到 WordPress.org 上的版本，如果这不是您实际想要的操作，可能会破坏他们的网站。

有时，当公司或个人私下发布插件（例如通过 Github）并决定在 WordPress.org 上重新发布时，会出现这种情况。在这些情况下，我们建议您通过电子邮件联系我们，我们将指导您如何解决此错误。

### 为什么我会收到错误提示，说我的插件名称不能以某个术语开头？

该错误提示是为了告知您，您的插件显示名称不能以他人的商标术语开头。这是为了保护您和插件目录免受商标滥用相关的法律问题。要解决此问题，您必须更改插件 readme 文件和主 PHP 文件中的显示名称。

请不要试图通过巧妙重命名插件（例如 WooCommerce 改为 WuuCommerce）来“规避”此限制。这样做只会让我们担心您将来可能无法遵守指南。

### 为什么我会收到错误提示，说不能在插件名称中完全使用某个术语？

部分商标持有者要求我们不再允许在插件名称中完全使用特定术语。若您看到此错误，则必须从插件名称中移除该术语。

> 要继续提交，您必须完全移除主插件文件和 readme 中 Plugin Name: 行内的 "\[TERM\]"。

若您试图通过将术语从 'Facerange' 改为 'Face-Range' 来规避此限制，我们将暂停您的提交并重申您不能使用该术语。请不要试图耍小聪明或取巧来绕过此限制。

### 如何提交官方插件？

请使用官方组织用户账户登录，并仅通过该账户提交。

我们如何确认您是官方组织？主要通过您的电子邮箱地址进行验证。

我们无法接受以个人开发者账户名义提交的代表组织的插件，除非这些账户也明确属于公司。例如，使用带有 Gmail 地址的用户提交官方插件，很可能会因商标侵权被标记。

### 如果我提交插件时使用了错误的用户 ID 怎么办？

请立即回复邮件告知我们。我们可以为您转移所有权。如果您忘记这样做，可以自行修复：[将正确的账户添加为提交者](#how-do-i-give-someone-else-access-to-my-plugin)，然后让该账户移除您自己的权限。

**请勿**重新提交您的插件。只需立即告知我们，我们会为您解决。

### 插件审核需要多长时间？

由于每个插件都不同，因此没有官方的平均审核时间。如果您的插件规模较小且代码完全正确，通常会在**十四**天内完成_初步审核_。

如果您的插件存在代码问题，审核时间将取决于您修正问题所需的时间。无论哪种情况，您都_会_收到来自 `plugins@wordpress.org` 的邮件通知审核状态，请务必将该邮箱加入白名单并耐心等待我们的回复。

### 我已提交修复但无人回复。我应该等待多久？

我们力求在十个（10）工作日内回复所有审核。如果时间未到，那只是因为我们最近非常忙碌。如果只过了两天，比如跨越周末或假期，那么你**合理**来说不应期待回复。

请记住，审核团队完全由志愿者组成，他们都有全职工作和其他志愿职责。我们会及时回复，但我们在 WordPress 之外也有自己的生活。

### 如果我的插件出现问题，我需要多长时间来修复？

没有具体时间限制，只要我们知晓您正在处理且认为您有所进展，审核就会保持开放状态。插件将在 3 个月后被拒绝，但审核通道仍会保留。

### 为什么我的插件在三个月后被拒绝了？

如果您的插件审核在三个月后仍未完成，我们将拒绝您的提交以保持队列的可管理性。在任何时间点，我们都有超过 500 人处于审核中期，我们认为三个月是一个相当合理的时间范围。

### 我的插件终于修复好了，需要重新提交吗？

若插件因三个月未回应而被拒，请重新提交并回复邮件告知我们您希望继续之前未完成的审核。即使已过去 18 个月也没关系——目前最长记录是 3 年。我们完全理解修复需要时间。

**但请注意**：若插件因其他原因被拒，请勿重新提交，直接回复原邮件即可。

### 我一次可以提交多少个插件进行审核？

通常只能提交一个。如果您是拥有超过一百万活跃插件安装量的插件作者，我们理解您有更多正在进行的项目，因此您将有不同的限制，最多可以同时提交 10 个插件。

### 为何不能同时提交多个插件？

实践证明，允许用户同时提交多个插件会对审核流程产生不利影响。经常在所有插件中发现错误，导致相同的邮件被多次发送。此外，用户常常混淆正在处理的审核任务，使待解决问题变得模糊不清。改为一次只处理一个插件后，这类混淆情况显著减少。

此外，许多新用户不了解如何使用 SVN，最终提交了多个插件却从未使用过任何一个。这会消耗我们的资源，因此我们确实需要限制用户。

由于所有插件都会在四周内完成初步审核，这不应造成太大困难。

### 我可以用多个账户提交多个插件吗？

不可以。如果这样做，我们将暂停您的所有次要账户。请不要试图规避一次只能提交一个的规则。

### 我需要我的插件在特定日期前获得批准，该怎么办？

请尽早提交。除非插件旨在解决安全或法律问题，否则我们不允许插队。如果确实涉及此类问题，请发送邮件至 `plugins@wordpress.org` 并说明情况。

### 有哪些具体事项需要避免？

我们关注一些相当明显的问题，所有这些都在[我们的指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)中列出。大多数可以概括为"不要成为垃圾信息发送者"，但这里重点提及最常见的几种情况：

- 作为服务时未包含 `readme.txt` 文件
- 未使用 `WP_DEBUG` 测试插件
- 包含自定义版本的打包 JavaScript 库
- 不必要地调用外部文件
- "技术支持"链接
- 回传数据

再次强调，这只是简要概述。请务必阅读完整指南，因为详细清单非常全面。

### 有哪些插件不被接受？

我们不接受那些“毫无功能”、非法或助长不良行为的插件。这包括黑帽SEO垃圾插件、内容伪原创工具、仇恨插件等。

同样，我们也不接受框架插件或库插件。如果你的插件必须依赖其他插件或主题进行自我修改才能使用，那么它就是一个库。如果你的插件是一个模板，通过直接定制文件可以构建更多代码，那么它就是一个框架或样板。框架和库应该与每个插件一起打包（最好以不与其他使用该框架或库的插件冲突的方式）。至少在核心支持插件依赖之前如此。

我们也不接受完全复制他人作品的插件，或重复WordPress核心功能的插件。基本上，你的插件应该提供新功能、新方法，或解决特定问题。

### 我想重做、升级或重塑现有插件。直接重新提交就行了吧？

不行，你应该重写并升级现有插件，将其作为主要版本发布。我们无法重命名插件或转移用户，因此新插件不会继承任何现有用户、评论、支持话题、评分、下载量、收藏等。基本上，你会让所有现有用户陷入困境，这很不友好。

### 我提交时出了错，该如何修正？

您随时可以在提交页面更新您的插件文件。

提交后，您可以修改一次插件别名。

每次提交后都会收到一封包含指引的自动邮件。如果您遇到其他问题，请回复该邮件或发送邮件至 `plugins@wordpress.org` 说明情况。

关于插件别名，如需进一步修改，您需要联系我们。我们也会在审核前尽量检查名称中的拼写错误，但我们也难免会有疏漏。

### 插件名称有哪些限制？

我们有以下规定：

- 插件名称或别名中不得使用粗俗词汇
- 除非特殊情况，插件别名中不得包含"WordPress"或"Plugin"
- 插件别名中不得使用版本号
- 由于系统限制，别名仅允许使用英文字母和阿拉伯数字
- 插件名称不得**以**或包含可能引起混淆的商标术语或特定项目/库/工具名称，除非由官方代表提交

我们鼓励大家发挥创意，构思独特的别名。对于任何使用不可接受别名的插件，我们将自动进行修正。若对最佳选择存有疑问，我们会与您联系确认。

## 使用 SVN 仓库

### 我应该把文件放在哪里？

请将您的代码文件直接放在仓库的 `trunk/` 目录中。每当您发布新版本时，请通过将当前主干修订版复制到 `tags/` 目录的新子目录中来[标记该版本](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/#task-3)。

请确保更新 [`trunk/readme.txt`](https://wordpress.org/plugins/developers/#readme) 以反映**新的**稳定版本标签。

自述文件所需的图片（例如[截图、插件标题和插件图标](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/)）应放在您 SVN 检出根目录下的 `assets/` 目录中（您可能需要创建此目录）。例如，该目录将与 `tags/` 和 `trunk/` 处于同一层级。

### 我能否将文件放在 trunk/ 的子目录中？

不可以。这样做会导致 zip 生成器失效。

如果你的插件结构复杂、文件众多，当然可以将它们组织到子目录中，但 [readme.txt 文件](https://wordpress.org/plugins/developers/#readme) 和主插件文件应直接放在 `trunk/` 目录下。

### 如何命名我的标签（即发布版本）？

您的 Subversion 标签应看起来像版本号。具体来说，它们应仅包含**数字和句点**。`2.8.4` 是一个好看的标签，`my neato releaso` 则是一个难看的标签。我们建议您使用[语义化版本控制](https://semver.org)来跟踪发布版本，但我们并不强制执行此规则。

请注意，这里讨论的是 _Subversion_ 标签，而非 `readme.txt` 搜索类型的标签。

### 我应该在 SVN 中保留多少个旧版本？

越少越好。几乎没有人需要你发布仓库中的旧代码。记住，SVN **不是**用来进行代码版本控制的。你可以使用 Github 来处理这类事情。SVN 应该只存放你当前的发布版本，你不需要保留所有旧版本的所有次要发布。只保留每个旧版本的最后一到两个版本就足够了。

### 我可以在插件中包含 SVN 外部引用吗？

不可以。您可以将 [svn externals](https://svnbook.red-bean.com/en/1.0/ch07s03.html) 添加到您的代码仓库中，但它们不会被添加到可下载的 zip 文件中。

### 我可以在插件中放入压缩包和其他压缩文件吗？

不可以。

### 我可以包含压缩后的 JS 文件吗？

可以！但您必须在插件中保留未压缩版本，或者通过自述文件告知用户何处可以获取未压缩文件。

允许压缩代码，但禁止隐藏代码。所有提交至此目录的代码都必须具备人工可读性。

## 您的 WordPress.Org 页面

### 我的插件何时“上线”？

一旦您将代码推送到 SVN 文件夹，您的插件就会上线。**请勿**在未准备好时推送代码，因为除了[关闭插件](#closed-plugins)外，没有“关闭”开关。由于关闭插件是永久性的，我们建议您在准备好上线之前不要推送代码。

### WordPress.org 插件目录的数据来源是什么？

数据来源于您在插件文件和 [readme.txt 文件](https://wordpress.org/plugins/developers/#readme)中指定的信息，以及 Subversion 代码库本身。请阅读[关于 readme.txt 如何工作](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/)以获取更多信息。

您还应充分利用主插件文件中的[插件头部信息](https://developer.wordpress.org/plugins/the-basics/header-requirements/)。这些信息将定义您的用户名在 WordPress.org 托管页面以及 WordPress 管理后台中的显示方式。我们建议使用所有这些头部信息来完整记录您的插件。

### 我能否指定 WordPress.org 插件目录应使用哪个版本的插件？

可以，通过在主干目录的 [readme.txt 文件](https://wordpress.org/plugins/developers/#readme)中指定 `Stable Tag` 字段即可。

我们要求您**不要**使用 'trunk' 作为稳定标签，因为这会使回滚操作变得比实际需要更复杂。

### "Tested Up To" 值应设为哪个 WordPress 版本？

理论上，应填写你实际测试过的最高版本。但切勿超过当前的候选发布版。若无候选版，则不超过当前活跃版本。例如，若 WordPress 稳定版为 6.0.9，你可填写 6.0 至 6.0.9 之间的任何版本。若存在 6.1-RC 候选版，则可填写 6.1，但绝不可超过此版本。

切勿自作聪明填写 6.5 或 7 等未来版本，这将导致页面报错。

### 每次更新自述文件都需要发布插件新版本吗？

不需要。如果只是对自述文件或图标/标题进行外观调整，您**无需**发布新版本。只需确保更新 trunk 和 tag 文件夹即可。

### 每次更新代码都需要发布新版本吗？

是的。否则用户无法获取更新。

### 更新日志应包含哪些内容？

更新日志是记录插件所有或重大变更的文档，包括错误修复、新增功能等变更记录。如需更新日志格式指导，我们推荐使用 [Keep A Changelog](https://keepachangelog.com/en/1.1.0/) 格式，这是目前众多产品采用的通用格式。

### 应在更新日志中保留多少个版本？

始终在更新日志中保留当前主要版本。例如，若当前版本为 3.9.1，则需保留 3.9.1 和 3.9 版本。旧版本应移除并迁移至 `changelog.txt` 文件，这样既方便用户查阅，又能保持自述文件简洁相关。最多在自述文件的更新日志中保留插件的最新版本及上一个主要版本。`changelog.txt` 文件**不会**在 WordPress.org 插件目录中显示，但这并无大碍，因为多数用户仅关注最新动态。

### 如何在插件描述页面中嵌入视频？

对于 YouTube 和 Vimeo 视频，只需在描述中单独一行粘贴视频链接。请注意，视频必须设置为允许嵌入才能生效。对于 WordPress.com VideoPress 服务托管的视频，请使用 `[wpvideo]` 短代码。如有需要，短代码也可用于 YouTube 和 Vimeo 视频，就像在 WordPress 中一样。

### 为什么我的插件显示未在最新 WordPress 版本中测试？

当您在 readme 文件中未正确设置 'Tested Up To' 值时会出现此情况。该值应为您测试插件时所用的最新 WordPress 版本。若当前最新 **主** 版本为 4.9，则应填写 `4.9` 以表明兼容性。次版本更新无需修改（若您的 readme 兼容 4.9，则自动涵盖 4.9 至 4.9.1000 的所有版本）。

请注意，若填写未发布的 WordPress 版本（如 6.0），同样会显示此提示信息。

### 插件目录需要多长时间才能反映我的更改？

WordPress.org 插件目录每隔几分钟更新一次。但根据更新队列的大小，您的更改可能需要更长时间才能显示。请至少等待 **6 小时** 再联系我们。

### 如何为我的插件页面制作一个酷炫的横幅？

您可以通过将正确命名的文件上传到 `assets` 文件夹来制作自己的[插件头部横幅](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-headers)。阅读[关于插件头部横幅](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-headers)以获取更多信息。

### 如何制作插件图标？

您可以通过将正确命名的文件上传到 `assets` 文件夹来制作自己的[插件图标](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons)。如需更多信息，请阅读[关于插件图标](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons)的说明。

### 我可以在插件横幅/图标中使用官方徽标吗？

通常不可以。

你的插件图标绝不应直接使用未经修改的官方徽标（例如 Facerange 的徽标），这将侵犯其知识产权。你不可以在横幅或图标中使用官方徽标进行品牌宣传。即使你在自己的网站上获得了使用许可，我们在此处也并未获得相应授权。

与插件命名类似，我们建议你的图标和横幅设计应具有独特性。独特的设计往往更容易让人记住。

### 我可以在自述文件中使用多少个标签？

根据指南，[插件在其自述文件中最多只能使用 12 个标签](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#12-public-facing-pages-on-wordpress-org-readmes-must-not-spam)。这是为了控制垃圾信息。也就是说，出于同样的原因，只有前 **五个** 标签会在 WordPress.org 上显示。前 12 个标签用于搜索，其余标签将被忽略，因此堆砌标签对你毫无帮助。

此外，任何只有你一个人使用的标签都不会显示，因为它们无助于他人找到另一个类似的插件。

## 插件名称

### 插件审核通过后能否更改名称？

可以，但有限制。您可以更改显示名称，但插件审核通过后，其 _slug_（即插件 URL 中属于您的那部分）无法更改。这就是为什么我们在提交过程中多次提醒您。

要更改显示名称，请编辑您的主插件文件，将 "Plugin Name:" 的值更改为新名称。同时建议您同步更新 readme.txt 文件中的标题以保持一致。

### 为什么我不能使用他人的商标/品牌作为插件名称？

简而言之，因为你不是他们。

如果你为 BooCommerce 编写了一个附加插件，你不能将其命名为 "BooCommerce Improved Product Search"，因为这会生成 slug `boocommerce-improved-product-search`，从而与 'BooCommerce' 的商标冲突。不过，提交名称 "Boo Improved Product Search" 是可以接受的，它将使用 slug `bc-improved-product-search`（"bc" 未被注册商标）。

再举一个例子，如果你有一个插件，将某项服务与一家名为 Amazorn 的流行云托管公司集成，你可以将其命名为 "My Service Integration for Amazorn"，但**不能**使用 "Amazorn – My Service Integration"。

考虑现实中的 Keurig 例子。如果你制作了一个环保的冲泡杯，你可以将其营销为 "EcoBrew Pod for Keurig"，但**不能**尝试将其营销为 "Keurig EcoBrew Pod"。后者暗示与 Keurig 有直接关系，在某些国家实际上是违法的。为了保护你，我们需要你在使用知名品牌名称和商标时谨慎行事。宁可过于谨慎；如果他们来找我们，要求关闭你的插件，因为你在显示名称中**首先**使用了他们的术语，我们必须照做。

_注意：我们不再允许新插件在其固定链接中使用 `woo` 开头，而是必须强制使用 `wc`。_

### 公司能否授权我在永久链接中使用其商标？

不能。

尽管我们理解公司可以也确实会授予使用许可，但我们出于一个非常重要的原因不接受将其用于永久链接：一旦插件通过审核，我们 _**无法**_ 更改您的永久链接。这意味着，如果公司后来改变主意并撤销许可，该插件将被关闭，所有用户都将被放弃。

为了前瞻性地确保插件在目录中的长期存续，我们不接受“许可”。除非是官方所有者，否则永久链接不得以商标（或广为人知的品牌/术语）开头。

### 能否更改插件的 URL/别名？

插件一经批准，其 URL 便无法更改，我们在审核流程中已通过多处提醒告知此事。

因此，我们拒绝大多数仅为了获得更好别名而提交的“新”插件替换旧插件的请求。

这是因为我们无法在插件之间迁移用户，也无法重定向流量。这意味着，提交新插件来更改别名将对插件的 SEO 和声誉造成极大损害，因为用户将被遗弃。大多数插件实际上并不需要新的 URL，而只是希望编辑其显示名称。

除非您的别名存在严重拼写错误、语言问题或法律问题，否则我们**不太可能**批准新的别名。如果我们确实批准了，我们将标记您的账户，注明未来更名请求将被拒绝。

### 如何更改插件的显示名称？

您需要在自述文件 _和_ 插件主文件中进行更改。

### 显示名称可以随意设置吗？

请勿使用粗俗、侮辱性或任何蓄意冒犯的语言。若非官方来源，不得声称或看似声称自己是官方。例如，如果您开发了一个连接 Frozbaz 服务的插件，应将其命名为“Frozbaz 服务连接器”——这样能明确表明您是为该服务开发插件，而非代表服务本身。

若整合多项服务（例如为热门电商插件添加支付网关），我们强烈建议您构思一个原创且独特的显示名称。

### 我可以在展示名称中使用 WordPress 或 Plugin 吗？

目前可以，但不建议这样做。这完全是多余的，对 SEO 没有任何实际帮助。我们已经在页面标题中包含了 WordPress 和 Plugin。

### 我应该在插件名称中使用商标或注册符号吗？

假设您确实申请了商标，您当然可以这样做，但这并不常见。即使是谷歌或脸书也不这样做。只要使用您的商标术语并保留相关记录（如您的 SVN 日志），您通常就完成了保护品牌所需的法律行动。具体细节请咨询律师。

## 搜索

### 我的插件需要多久才能在搜索结果中显示？

通常插件提交到 SVN 后需要 6 到 14 天。这是因为我们需要添加您的数据、解析数据，并将其分发到我们所有**高度**缓存的服务器上。这个过程不是即时完成的。此外，作为新插件，我们没有使用数据，因此您可能需要等待一段时间。

### 如何提高排名？

为语言编写一份优秀的自述文件，及时回复支持帖子，获得好评。

### 我的 URL 和显示名称哪个权重更高？

两者都不。为了让效果最大化，请让你的显示名称令人难忘且具有描述性，同时保持在 5 个词以内。

## 支持论坛

### 如何获取论坛帖子通知？

访问 `https://wordpress.org/support/plugin/YOURPLUGIN` 并查看右侧边栏。点击“订阅此插件”按钮即可接收邮件提醒。

### 如何获取所有插件的通知？

每个插件支持论坛页面顶部都有一个"订阅"按钮。点击该按钮后，您将通过电子邮件接收通知。您可以在 `https://wordpress.org/support/users/YOURID/subscriptions` 查看已订阅的插件论坛。

对于 RSS 订阅，访问 `https://wordpress.org/support/view/plugin-committer/YOURID` 将列出您拥有提交权限的所有插件的支持请求和评论。如果您不是提交者，只是被列为作者？请使用 `https://wordpress.org/support/view/plugin-contributor/YOURID`

您还可以访问 `https://profiles.wordpress.org/YOURID/profile/notifications/` 并设置任何您希望通过电子邮件接收通知的关键词。请注意，如果使用通用关键词，可能会导致通知过多。

### 如何为支持账户授予插件访问权限？

您可以将支持代表添加到您的插件中。支持代表可以将论坛主题标记为已解决或置顶（与插件作者和贡献者权限相同），但无权提交代码到插件库。

管理插件支持代表的界面位于插件页面的“高级视图”中，紧邻代码提交者管理功能。当某人被添加为支持代表后，他们在回复插件支持主题或评论时将获得“插件支持”徽章。

### 你们会删除针对我插件的差评或负面评论吗？

通常不会。评论反映了用户对您产品的使用体验。如果他们不喜欢，我们无权更改。如果您认为某条评论无效（例如针对其他插件），请使用帖子上的 `modlook` 按钮。**论坛**团队的一名成员将进行调查。

滥用 modlook 功能可能导致您的插件被暂停。请明智使用此功能。

### 什么是“马甲账号”？

当有人在论坛上创建多个账户时，通常是为了给自己刷五星好评，或制造虚假支持工单以显得响应迅速。马甲行为违反我们的准则，将导致评论和帖子被删除，也可能导致您的账户和所有插件被移除。请勿这样做，也不要公然指责他人这样做。

## 已关闭插件

### 如何关闭我的插件？

自 2020 年 4 月起，您可以随时关闭自己的插件。为此，请转到插件页面上的 **高级** 选项卡（即 `https://wordpress.org/plugins/myplugin/advanced/`），然后向下滚动到 **关闭此插件** 部分。您将在此处看到一条警告信息和一个按钮。

![“关闭此插件”功能的图片，附有说明：“警告：关闭插件是一项旨在永久生效的操作。您将无法在不联系插件团队的情况下重新打开它。”下方是一个写着“我明白了”的按钮。](https://i0.wp.com/developer.wordpress.org/files/2020/04/HowtoClose.png?resize=1024%2C275&ssl=1)

如果您同意警告并希望关闭插件，请按下该按钮。

请记住，除非您能证明您的情况合理，否则您的插件**将不会**被恢复。根据请求关闭插件旨在成为**永久性**操作。

### 如果我意外关闭了插件怎么办？

请发送邮件至 `plugins@wordpress.org` 并请求重新开放您的插件。不过，我们可能会询问您是如何操作的，以便我们改进该功能。

### 为什么无法关闭我自己的插件？

假设您已使用正确的账户登录，这很可能是因为您的插件用户数量过多。如果插件用户超过 10,000 名，您需要发送邮件至 `plugins@wordpress.org` 并请求我们协助关闭。

### 我可以暂时关闭我的插件吗？

不可以。

我们不允许这样做，因为这会给用户带来糟糕的体验。隐藏插件会让用户认为该插件因安全或准则问题而被下架，从而导致他们不再信任你。我们无法阻止他们的想法，因此我们禁止“暂时”关闭。

通常人们希望在插件存在正在修复的错误或无法提供支持时这样做。我们建议你尽快修复错误，或者如果你无法支持该插件，请更新自述文件，说明其目前不受支持及原因。

如果这是一个全新的插件，你应该将其称为“公开测试版”，以便人们了解其状态。

### 插件关闭后会发生什么？

当插件被关闭时，页面将显示为关闭状态，并且不再生成压缩包。用户将无法通过网站下载该插件，也无法通过 WordPress 管理后台进行安装。根据目录原则，SVN 代码库将保持可访问状态，以便其他人按需下载和分叉代码。

60 天后，关闭信息将更新为提示关闭原因，但仅以最宽泛的术语（如违反准则、安全问题等）说明，不会提供具体细节。

### 为什么我的插件被关闭了？

插件可能因违反准则、存在安全问题或作者主动要求而被关闭。若涉及侵权、滥用或安全等活跃问题，所有对该插件拥有提交权限的账户都会收到通知。

如果插件在 6 个月内从未被使用（即未向 SVN 推送任何代码）、SVN 损坏超过 12 个月，或插件自述文件显示其已弃用，我们*可能*在不通知的情况下关闭插件。

### 为何他人的插件被关闭？

自 2017 年起，插件关闭原因已在插件数据库中记录。插件关闭六十天后，关闭原因将对外公开：

![已关闭插件示例，原因为“作者要求”](https://i0.wp.com/developer.wordpress.org/files/2015/04/not-hello-dolly.jpg?resize=1058%2C526&ssl=1)

请注意：我们不会公开披露插件被关闭的具体细节。

### 我能让他人的插件被关闭吗？

如果您向 `plugins@wordpress.org` 报告插件中的[安全问题](https://developer.wordpress.org/plugins/wordpress-org/plugin-security/reporting-plugin-security-issues/)或[准则违规](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)，我们将进行审查并采取适当措施。大多数情况下，这会导致插件被关闭。除非您主动要求，否则您的姓名不会被公开，以保护您免受反弹影响。

### 有人发布了我的插件副本！我该怎么办？

请发送邮件至 `plugins@wordpress.org`，并提供被盗插件的链接。同时，请附上您自己插件的下载链接或直接附上 zip 文件。我们将对比两个文件以及我们拥有的所有编码历史记录，以确定该插件是否确实属于盗窃行为，或者仅仅是一个未注明来源的分支。

请注意，如果您将插件授权为 GPLv2 或更高版本，那么只要版权信息保持不变且您被署名，他人分支您的作品是完全允许的。

### 如果有人复制了我的代码但未注明出处，我该怎么办？

请立即发送邮件至 `plugins@wordpress.org`！**特别是**当你的代码采用非 GPL 许可证时。虽然我们允许他人分叉其他插件并将其代码纳入自己的插件中，但必须始终注明来源。版权归属和出处声明是强制要求。

### 你们是否会因品牌/商标侵权而关闭其他插件？

我们尽力维护版权和商标要求，并防止品牌混淆。在插件获得批准前，我们通常要求开发者对一些较为明显的部分进行修改。然而，考虑到目录中有 60,000 个插件，且某些术语相当常见（如“popup”或“all-in-one”），URL 或名称的“差异”程度是有限的。因此，我们要求开发者更改插件的**显示名称**，以避免冲突或混淆。

如果有人明显侵犯了您的版权、商标或现有品牌，无论是通过显示名称还是使用受商标保护的图像，请发送邮件至 `plugins@wordpress.org` 并提供相关证据，我们将联系开发者并要求其进行修改。

我们期望这些请求是**合理**的。也就是说，如果您向我们投诉并列出 12 个使用“最佳联系表单”这一术语的插件，仅仅因为这是您插件的名称，我们将审查这些插件，并仅在它们过度使用该短语时才会关闭它们。如果它们仅使用一次（例如“这是法罗群岛的最佳联系表单插件”），则是可以接受的。如果它们进行关键词堆砌，我们更可能因关键词堆砌而关闭它们。简而言之，如果您的插件名称非常通用，这种情况很常见，通常**不**构成侵权案件。

另外请注意，如果这不是**您**的商标，我们无法接受您的报告。管理和维护商标是商标所有者的责任，而非其用户的责任。

### 如何提交安全报告？

请将清晰简洁的问题描述发送至 `plugins@wordpress.org` 邮箱。[详情请阅读我们关于报告安全问题的文档](https://developer.wordpress.org/plugins/wordpress-org/plugin-security/reporting-plugin-security-issues/)。

### 你们是否为插件漏洞发现提供赏金？

不提供。我们与任何漏洞赏金计划均无关联，因此不会将您的报告提交给他们。我们唯一合作的平台是 [hackerone.com/automattic](https://hackerone.com/automattic)，且仅限与 Automattic 资产相关的漏洞。其他所有情况请自行处理，请勿要求我们代为提交。

### 你们是否协助提交或提供 CVE？

不。我们没有能力协助处理 CVE。

### 我的插件被关闭了，可以重新开放吗？

也许可以。如果是因为安全问题被关闭，请修复问题并回复邮件，大多数情况下我们会重新开放插件，除非它存在更多安全问题或严重的准则问题。如果是因为违反准则被关闭，这取决于违规的严重性和性质。例如，重复违规者比初犯者更不可能重新开放插件。

如果你主动要求关闭插件，需要解释为何改变主意。当开发者请求关闭插件时，插件应保持关闭状态，而不是一个月后又重新开放。

**所有**插件都必须通过当前的标准和安全审查才能恢复。这是强制要求。多次关闭插件会让用户对你失去更多信任，不如一次性彻底解决所有潜在问题，即使关闭时间更长。

### 为什么我的插件因员工/同事违反规定而被关闭？

从技术支持到开发者，所有代表插件的人员都由插件所有者负责。如果他们严重违反规定，所有者应承担相应后果并纠正行为。若未能做到这一点，插件将被关闭。我们会通知插件所有者并解释原因，同时尽力保持插件开放。

### 我的所有插件都被关闭了！如何恢复？

我们极少会关闭开发者所有的插件。通常发生这种情况是由于以下原因：

1.  您要求我们关闭您的所有插件
2.  电子邮件问题
    1.  邮件被退回，我们无法联系到您
    2.  邮件发送了自动回复，且我们至少发送了两次警告要求修复
3.  准则问题
    1.  之前因行为问题受到过谴责和/或收到最终警告
    2.  向目录和/或志愿者发出法律威胁
    3.  违规行为被视为"极其恶劣"（死亡威胁、大量傀儡账号、骚扰等）

如果您要求我们关闭插件，您需要解释**为何**改变主意。

如果您遇到电子邮件问题，您必须解决这些问题，并且需要将您的所有插件更新到当前的安全和准则标准。

至于最后一种情况……通常您无法恢复。如果我们因您的行为向您发出最终警告，并且在不到一年的时间内，您再次出现同样的问题（或未能解决我们提到的所有问题），我们将不会重新开放您的插件。

### 我刚刚收到了最终警告，该怎么办？

首先，**请认真对待此事**。邮件会详细列出存在的问题以及我们决定升级为最终警告的原因。插件所有者需要解决所有问题，停止违反新规定，并密切监控所有协作者的行为。简而言之，停止违反准则、停止找借口、为不当行为道歉并纠正方向。

我们最不希望做的就是封禁某人并禁用其所有插件。这对社区不利。但与此同时，如果开发者无法或不愿遵守与其他所有人相同的规则，将其保留在目录中是有害的，也是对他人不尊重的表现。

## 插件所有权

### 如何让他人访问我的插件？

要将用户添加为提交者（即授予他们更新代码的权限），请访问 `https://wordpress.org/plugins/YOURPLUGIN/advanced` 并将他们的用户名添加为提交者。

若希望他们显示为作者，请将其用户名添加到 `readme.txt` 文件中。

**请勿将普通用户添加为作者。** 此角色仅适用于协助开发的人员。这意味着如果有人“启发”了你，你不应将其添加为作者。

### 如何移除他人对我插件的访问权限？

任何拥有提交权限的人都可以执行此操作。前往 `https://wordpress.org/plugins/YOURPLUGIN/advanced` 并将鼠标悬停在其 ID 上。会出现一个删除链接，点击它即可。

请不要删除您自己的权限。

### 如何更改插件所有者？

前往“高级”选项卡并向下滚动至“危险区域”。您将在此处看到 **“转移您的插件”** 部分。从下拉菜单中选择接收者并点击按钮。

更多详细信息，请阅读[关于转移插件的文档](https://developer.wordpress.org/plugins/wordpress-org/transferring-your-plugin-to-a-new-owner/)。

### 我尝试转移插件但系统提示无法操作，这是为什么？

拥有大量用户（超过 10,000 名）或被认定为 WordPress 项目核心的插件（例如精选或测试版插件）仅能通过向插件团队提交书面申请进行转移。[请查阅插件转移文档了解详情](https://developer.wordpress.org/plugins/wordpress-org/transferring-your-plugin-to-a-new-owner/)。

### 如何接管被遗弃的插件？

[我们允许用户接管当前不再开发的现有插件](https://developer.wordpress.org/plugins/wordpress-org/take-over-an-existing-plugin/)。

我们建议您首先尝试联系原始开发者，以便他们可以将您添加为维护者。在某些情况下，这不可行，您应该从修复插件开始。确保它符合编码标准、安全可靠，并更新版权信息以包含您自己。然后，您可以联系我们讨论[插件接管事宜](https://developer.wordpress.org/plugins/wordpress-org/take-over-an-existing-plugin/)。

我们**不**保证您会被授予任何人的插件，即使审查成功也是如此。

### 这些购买我插件的提议可信吗？

简短回答：很可能不可信。

许多开发者会收到主动发来的电子邮件或购买他们插件的提议。我们发现绝大多数此类提议都是欺诈性的，因此**不**建议您跟进。

虽然确实存在合法的购买提议，但它们通常来自与插件相关的官方公司，或来自一家成熟的插件公司。那些以“我们正在联系 WordPress 社区……”或“我们正在寻求收购现有的 WordPress 插件……”开头的提议不应被信任。此类收购常常通过采用诸如跟踪用户或其他严重违反准则的卑劣手段，破坏了插件（以及原始开发者）的声誉。

如果您确实选择出售您的插件（或将其转让给他人），请确保新所有者理解[代码库的所有准则](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)。如果他们违反我们的条款，插件将被移除，并且根据违规程度，我们可能不会将其归还。任何拥有插件提交权限的人，都对其用户行为拥有所有权和责任。发送垃圾邮件、插入跟踪数据以及添加无用功能是毁掉您插件的最快方式。

我们主张只将您的插件交给您**亲自**审查过并且信任其能对您的代码和用户负责的人。

### 当插件开发者去世时会发生什么？

当确认开发者已去世时，他们将从自己的插件中被移除，以防止不道德者获取访问权限并伤害用户。如果他们是唯一的开发者，插件可能会被关闭。我们会尽力寻找他们的朋友和同事，首先给予他们接管代码的机会，但如果找不到可靠或愿意接手的人，插件将被关闭。

# 管理您的插件安全性

WordPress 插件的代码安全性受到[高度重视](https://wordpress.org/about/security/)。

[warning]如果您发现插件存在安全问题，请阅读[报告插件安全问题](https://developer.wordpress.org/plugins/wordpress-org/plugin-security/reporting-plugin-security-issues/)。[/warning]

当 WordPress 安全团队确认插件存在漏洞时，他们会联系插件作者，指导其如何修复并发布安全版本。如果插件作者未及时回应或漏洞严重，该插件/主题将从公共目录中移除，某些情况下，安全团队会直接修复并更新。

## 修复安全问题

当收到插件存在安全问题的报告时，可能会令人恐慌。首先，不要惊慌。每个人都会犯错。最重要的是安全且及时地修复它。

1. 确保理解报告内容。如果不确定其含义，请要求提供详细信息。即使是第三方报告者通常也愿意花时间解释问题所在，并指导你研究正确的修复方法。
2. 尽可能保持更改范围最小化。这将使你后续审查更加容易。
3. 测试你的插件。确保安全修复不会破坏其他功能。确保升级不会导致奇怪的错误。保持 `WP_DEBUG` 开启并记录所有错误。
4. 在更新日志中记录该问题。无需包含具体细节，但需注明已解决安全问题。
5. 在 README 中鸣谢报告者。这是友好的做法，能促使人们未来更愿意免费帮助你。
6. 更新版本号。我们推荐使用[语义化版本控制](https://semver.org/)，因此针对插件 3.9 版本的安全更新应将版本号更改为 3.9.1，依此类推。

## 自动插件安全更新

自 WordPress 3.7 起，我们已具备推送插件自动安全更新的能力，以修复插件中的关键漏洞。许多站点已利用插件自动更新功能，无论是直接通过过滤器选择启用，还是使用众多可用的 WordPress 远程管理服务之一。

在极端情况下，插件团队和 WordPress 安全团队可能判定某个插件问题足够严重，必须为所有用户进行更新。这种情况极为罕见，因为潜在的冲突风险很高。

批准插件进行自动更新并将其推送给 WordPress 用户的过程高度依赖人工操作。安全团队会审查发布中的所有代码变更，验证问题及其修复方案，并确认插件触发更新是安全的。推出自动更新需要修改和部署 API 代码。这与核心安全版本的标准和流程相同。

### 评判标准

当前我们评估安全推送的考量标准是一个简洁的清单：

1. 安全团队是否已获悉该问题？
2. 问题的严重程度如何？对 WordPress 安装的安全性乃至更广泛的互联网会产生什么影响？
3. 问题的修复是否独立自洽，还是会添加大量无关的额外代码？
4. 如果插件的多个分支都受到影响，是否为每个分支都准备了发布版本？
5. 更新能否*安全地*自动安装？

这些要求以任何人都能逐项确认的方式定义。

第一条标准——让安全团队知晓问题——至关重要。由于这是一个严格受控的流程，WordPress 安全团队需要尽早收到通知。只需将详情发送邮件至 `plugins@wordpress.org` 即可告知我们。

插件团队和安全团队将与插件作者（以及报告者，若非同一人）协作，研究漏洞及其具体暴露方式，验证提议的修复方案，并确定将发布哪些版本以及发布时间。

## 常见问题

### 如何申请插件自动更新？

如果您认为您的插件拥有足够庞大的用户群，或相关问题具有重大意义，请在推送代码**之前**发送邮件至 `plugin@wordpress.org`。邮件中需包含待审核的修改补丁，并说明您认为应实现自动更新的理由。

### 我能否在自动更新中包含安全修复之外的其他更改？

除少数例外情况外，不可以。安全推送应**仅**包含与安全相关的内容。我们更倾向于（且多数情况下要求）插件发布版本**仅**修复安全问题，进行最少的代码修改，且不包含无关更改。

这使每个人都能快速审查变更，并对变更更有信心。同时，这也意味着对用户造成的干扰最小。

### 为什么插件 A 获得了自动更新，而插件 B 却没有？

这并非 WordPress.org 的偏见，只是我们一直使用的手动流程的遗留问题。如果我们收到问题警报，我们会努力处理。如果几天后才发现，推出修复的机会窗口通常已经过去，效果也会大打折扣。

### 如何禁用自动更新？

有多种方法可以禁用此功能。适用于[禁用核心自动更新](https://make.wordpress.org/core/2013/10/25/the-definitive-guide-to-disabling-auto-updates-in-wordpress-3-7/)的文章同样适用。任何禁用所有自动更新功能的方法都会阻止插件更新。如果您只想禁用插件更新，无论是针对所有插件还是单个插件，都可以通过一个过滤器调用来实现。

### 如果无法（或不想）修复代码怎么办？

您无需修复。您的插件将保持关闭状态，2-3个月后，插件页面会显示因安全问题而关闭。若您希望推送修复但保持插件关闭，我们也可以处理。只需回复邮件与我们沟通即可。

### 我只需要修复报告的问题吗？

是也不是。你_确实_需要修复报告的问题，但完成后，我们会重新审查插件的_全部_代码。如果发现更多问题，你同样需要修复它们。最终目标是确保重新开放的插件是安全的。

### 如果我的插件存在规范问题怎么办？

当用户违反其他规范时会出现这种情况，例如包含自己的 jQuery 副本或进行未记录的外部服务调用。这取决于其他问题的严重程度。如果只是您自己的 jQuery 问题，我们可能会重新开放插件并允许您自行修复。但如果您记录了插件的所有安装信息，则必须在重新开放插件前修正此问题。

# 报告插件安全问题

[warning]请勿向插件团队报告 WordPress 核心的安全问题。如需报告 WordPress 自身的问题，[请遵循报告安全漏洞的指引。](https://make.wordpress.org/core/handbook/testing/reporting-security-vulnerabilities/)[/warning]

如果您发现插件存在安全问题，请**切勿**在任何公开场合发布相关信息。即使官方安全跟踪网站已有相关报告，提高对安全问题的关注度往往会增加用户被攻击的风险，且很少能加快修复速度。

如需报告插件问题，请发送邮件至 `plugins@wordpress.org`，内容需包含：

- 对问题的清晰简明描述
- 具体插件的链接
- 您是否已亲自验证该安全问题
- **可选**——第三方网站上任何公开披露的链接

对于严重漏洞，请遵循负责任且合理的披露原则。在向我们报告插件问题**之前**，应尽一切努力直接联系开发者（尽管我们理解这可能存在困难——请先检查插件源代码，许多开发者会列出其邮箱）。若无法私下联系到开发者，请直接联系我们，我们将提供协助。

大多数插件在问题解决前会被关闭以防止新用户下载。因此，在插件更新前您可能**不会**收到修复通知。同时，由于资源有限，我们目前**不**提供协助提交 CVE 的服务。您可以自行提交，但我们无法提供帮助。

若您已在公开场合发布漏洞信息并提供了报告链接，请勿删除！我们将直接将该信息转交给插件开发者。

# 防止 WordPress 更新你的外部插件

[warning]本页信息**仅**适用于未托管在 WordPress.org 上的插件。请勿对托管在官方目录中的代码使用此方法。[/warning]

如果你的插件托管在 WordPress.org 上，我们会为你处理所有更新。因此，本文档中的步骤**严禁**用于提交到官方目录的任何插件。任何托管于此的插件若被发现包含此类代码，将被关闭，开发者必须移除该代码才能恢复插件。

我们选择在此记录此方法，旨在教育开发者，并确保全球 WordPress 社区更加安全。

## 始终使用规范的文件夹名称

在深入代码之前，我们必须强调：确保插件不会被 WordPress.org 的更新覆盖的最佳方法就是使用规范的名称。如果您为公司开发插件，请使用类似 `companyname-function-plugin` 的文件夹命名方式——例如，如果您为 FaceRange 工作并开发状态插件，可将其命名为 `facerange-status-plugin`

这不仅因为使用了禁止词汇“plugin”而无法通过审核，插件团队还会验证插件所有者是否**合法**代表 FaceRange。

## 更新 URI

自 WordPress 5.8 起，我们为 WordPress.org API 的更新检查机制新增了一项功能，并允许通过一个新的头部字段来阻止更新：更新 URI。

假设你为自己网站制作了一个插件，并为其指定了文件夹名称 `my-plugin`。这是一个通用的文件夹名称，很可能被其他人使用。由于其通用性，我们也不会允许你在我们的系统中阻止此名称。

更新 URI 头部可以添加到插件头部信息中。请在你的主插件文件中查找以下部分：

```
/**
 * 插件名称：我的酷炫插件
 * 插件 URI：https://example.com/my-plugin/
 * 描述：我的插件能实现酷炫功能。
 * 版本：1.0
 * 作者：团队
 * 作者 URI：https://example.com/
 * 文本域：my-plugin
 * 许可证：GPLv2
 * 许可证 URI：https://opensource.org/licenses/gpl-2.0.php
 */
```

要应用此功能，请添加一个新的 **更新 URI** 头部，并在其值中填入一个**非** WordPress.org 的 URI：

```
 * 更新 URI：https://example.com/my-updater/
```

你也可以根据需要将其设置为 `更新 URI：false`。只要其中不包含 `wordpress.org/plugins` 或 `w.org/plugins`，它就会受到保护。

## Filtering Updates

Another method, which is supported on older versions of WordPress, is to filter external API requests and discard any for your plugin.

This code, which was written by [Mark Jaquith](https://markjaquith.wordpress.com/2009/12/14/excluding-your-plugin-or-theme-from-update-checks/), can be added to your own plugin:

```
function example_hidden_plugin_12345( $r, $url ) {
    if ( 0 !== strpos( $url, 'https://api.wordpress.org/plugins/update-check' ) )
        return $r; // Not a plugin update request. Bail immediately.

    $plugins = unserialize( $r['body']['plugins'] );
    unset( $plugins->plugins[ plugin_basename( __FILE__ ) ] );
    unset( $plugins->active[ array_search( plugin_basename( __FILE__ ), $plugins->active ) ] );
    $r['body']['plugins'] = serialize( $plugins );
    return $r;
}

add_filter( 'http_request_args', 'example_hidden_plugin_12345', 5, 2 );
```

What that does is check if the update request is from the WordPress.org api, and if it matches the plugin folder and file name of _this_ plugin. If it does, the plugin is removed from the list of plugins to check for updates.

# 预览与蓝图

或许您尚未察觉，WordPress Playground 是一项令人惊叹的功能，它允许任何人在浏览器中安全地运行临时 WordPress 安装。它利用 WASM 技术，在您的网页浏览器内完整运行 WordPress 安装——包括 PHP、数据库等所有组件。无需服务器，无需安装任何软件。

目前 Playground 已支持从插件目录加载任意插件或主题；具体方法如下。

## 插件预览按钮

插件预览功能会在插件目录中为插件添加一个便捷按钮（需插件开发者启用）。点击该按钮将跳转至已安装该插件的 Playground 环境。该按钮位于下载按钮旁。

预览按钮默认不显示，必须由开发者明确启用。开发者可通过蓝图文件配置预览环境并安装依赖项（如其他插件和主题）。

## 启用插件预览

要使插件预览按钮对所有用户可见，需要满足两个条件：

1. 必须在插件 `assets` 文件夹的 `blueprints` 子目录中提供一个有效的 `blueprint.json` 文件。
2. 插件提交者必须在插件的“高级”视图中将插件预览设置为“公开”。

如果存在有效的 `blueprint.json` 文件，则预览按钮将仅对插件提交者显示。在这种情况下，它将如下所示：

![“测试预览”按钮允许插件作者一键展示其插件的功能。](https://i0.wp.com/developer.wordpress.org/files/2024/03/live-preview.png?resize=554%2C140&ssl=1)

它之所以被称为“测试预览”，是因为其目的就是允许插件提交者在 Playground 环境中测试其插件，并决定是否将其轻松提供给公众使用。

## 蓝图

蓝图是用于设置 WordPress Playground 实例的 JSON 文件。

它们可用于指定诸如 PHP 和 WordPress 版本、着陆页，以及（最重要的）一系列自动化步骤，例如登录、安装和激活插件与主题。

您插件的蓝图应通过 Subversion 提交到 `assets/blueprints/blueprint.json` 文件。最初仅支持一个蓝图文件，但我们预计未来将允许多个。

以下是一个简单的 `blueprint.json` 文件示例，您可以用作起点：

```
{
    "landingPage": "\/wp-admin\/edit.php",
    "preferredVersions": {
        "php": "7.4",
        "wp": "5.9"
    },
    "phpExtensionBundles": [
        "kitchen-sink"
    ],
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        }
    ]
}
```

此处使用的功能包括：

`landingPage`，用于指定预览加载时用户将着陆的页面 URL。

`preferredVersions`，用于指定 PHP 和 WordPress 的版本。

`phpExtensionBundles`，在此示例中指定我们希望提供最常见的 PHP 扩展（kitchen-sink）。

`steps`，用于告知 Playground 在显示着陆页之前要执行的操作。在此示例中，它将简单地让用户登录到 wp-admin。

以下是一个更高级的 `blueprint.json` 示例，展示了您可用于为用户创建丰富演示环境的更多功能：

```
{
    "landingPage": "/wp-admin/post.php?post=5&action=edit",
    "preferredVersions": {
        "php": "7.4",
        "wp": "5.9"
    },
    "phpExtensionBundles": [
        "kitchen-sink"
    ],
    "steps": [
        {
            "step": "login",
            "username": "admin",
            "password": "password"
        },
        {
            "step": "installPlugin",
            "pluginZipFile": {
                "resource": "wordpress.org\/plugins",
                "slug": "my-imaginary-plugin-dependency"
            },
            "options": {
                "activate": true
            }
        },
        {
            "step": "installPlugin",
            "pluginZipFile": {
                "resource": "wordpress.org\/plugins",
                "slug": "my-imaginary-plugin"
            },
            "options": {
                "activate": true
            }
        },
        {
            "step": "installTheme",
            "themeZipFile": {
                "resource": "wordpress.org\/themes",
                "slug": "my-imaginary-theme"
            }
        },
        {
            "step": "setSiteOptions",
            "options": {
                "some_required_option_1": "your_favorite_values",
                "some_required_option_2": "your_favorite_values"
            }
        },
        {
            "step": "runPHP",
            "code": "<?php require_once 'wordpress/wp-load.php'; wp_insert_post(array('post_title' => 'wp-load.php required for WP functionality', 'post_status' => 'publish')); ?>"
        }
    ]
}
```

# 使用生成的蓝图文件

您可能会在插件页面上看到类似这样的提示：

```
您的插件尚未包含用于用户预览的蓝图文件。如需启用预览功能，请按以下步骤创建蓝图：

1. 在 Playground 中测试您的插件
2. 修复插件中导致 Playground 运行异常的错误
3. 下载 blueprint.json 文件
4. 将蓝图文件提交至 SVN 仓库
```

__测试__ 链接将使用自动生成的蓝图文件在 Playground 中加载您的插件，其中包含默认配置值和步骤。__下载 blueprint.json__ 链接可让您下载该自动生成的 `blueprint.json` 文件，您可以根据需要修改此文件，并在插件准备就绪后提交至 Subversion 仓库以启用 Playground 预览功能。

# 提交蓝图到 Subversion

你必须将 blueprint.json 文件提交到插件的 assets 文件夹中，命名如下：

`/assets/blueprints/blueprint.json`

# 发布确认邮件

为了更好地保护插件免遭意外发布，我们为插件新增了一项**可选**功能：发布确认。

当插件启用此功能后，要发布新版本，您需要像往常一样在 SVN 中标记新版本，然后在[发布管理](https://wordpress.org/plugins/developers/releases/)面板上进行确认。

发布管理面板的访问权限仅限于插件提交者，所有操作都需要通过令牌化链接进行确认，该链接会根据需要通过电子邮件发送给您。

当 WordPress.org 检测到 SVN 中标记了新版本时，所有提交者都会自动收到一封电子邮件，通知他们有新版本待发布、发布者是谁，以及一个指向确认发布的面板链接。
插件提交者还将在其 WordPress.org 插件页面顶部看到一个横幅，引导您前往发布管理面板。

一旦确认，发布将照常进行，并应在几分钟内更新。

## 要求

-   插件必须使用标签来标记新版本，不能从主干（trunk）发布插件。
-   您仍然必须更新主干/readme.txt 文件中的 `Stable Tag:` 标头。
-   一旦发布，已标记的版本将无法进行任何修改。这包括更改 readme 文件（例如“测试至”版本）。
-   提交者必须能够直接接收电子邮件，而不是将其发送到共享收件箱或支持工单系统。

## 需要双重批准

发布确认是可选的，但只需要一位提交者确认发布即可。
如果某个团队希望要求多位提交者批准发布，请向插件团队提出您的请求。

# 特殊用户角色与权限

每位为插件推送代码或提供支持的人员都必须拥有**自己的**独立账户。这些账户不必是个人身份信息（例如，您可以将其命名为 PluginNameSupport1），但出于保护您自身的目的，所有账户必须由单个人使用。

对于插件，用户可拥有四种角色。所有角色都可以在插件页面的**高级视图**部分进行管理：

![插件页面界面，其中“高级视图”链接被高亮显示。](https://i0.wp.com/developer.wordpress.org/files/2020/08/advanced-view.jpg?resize=300%2C260&ssl=1)

根据需要，有字段可添加支持代表和提交者。

## 所有者

插件所有者由提交插件的人员自动设置。插件获批后，该人员将被添加为提交者（见下文）并标记为所有者。如需更改所有者，请向下滚动至 **危险区域** 部分，从下拉菜单中选择新所有者：

![转移此插件界面，包含新所有者选择器和“请转移 -插件名称-”按钮](https://i0.wp.com/developer.wordpress.org/files/2020/08/can-transger.jpg?resize=1024%2C548&ssl=1)

如果没有其他具有提交权限的用户，您需要先授予他们访问权限，然后才能转移插件。请记住，插件所有者应 **始终** 拥有其名下插件的提交权限。

如果您看到此消息，则说明您不是当前所有者，需要联系当前所有者进行所有权转移：

![消息：此插件当前由 -用户- 所有，他们可以选择将插件的所有权转移给您](https://i0.wp.com/developer.wordpress.org/files/2020/08/Owner.jpg?resize=1024%2C249&ssl=1)

如果原始所有者已无法联系，您可以联系插件团队寻求帮助。

## 提交者

拥有提交权限的人员能够通过 SVN 推送代码，并可就插件向插件目录团队提出正式请求。

任何拥有提交权限的人员都有权请求关闭插件，并能够添加或移除其他人员的提交权限。此操作可通过侧边栏的**高级页面**完成：

![添加提交者的界面，包含一个输入框和旁边的“添加”按钮](https://i0.wp.com/developer.wordpress.org/files/2021/02/Commit.jpg?resize=302%2C133&ssl=1)

在论坛中，这些人被标记为“插件作者”，并能够将与其插件相关的帖子标记为已解决。

除了在论坛中回复插件支持主题时显示的“插件作者”标签外，拥有提交权限并不会对外公开显示。若要在插件的“贡献者与开发者”部分列出，并将插件包含在 WordPress.org 个人资料中，用户必须被列为贡献者（请参阅后续章节）。

添加和移除提交权限只能由现有的提交者执行。

## 支持代表

支持代表**不具备**直接管理插件本身的额外权限。他们无法请求更改插件在目录中的状态。不过，他们将在论坛中被标记为官方支持人员，这有助于用户了解是谁在提供帮助。

![添加支持代表的界面，包含一个输入框和旁边的“添加”按钮](https://i0.wp.com/developer.wordpress.org/files/2021/02/Support.jpg?resize=317%2C140&ssl=1)

在论坛中，他们被标记为“插件支持”，并有权将与自家插件相关的帖子标记为已解决。

他们会在插件页面上显示，且该插件会作为支持代表出现在他们的个人资料页中。

此状态的添加和移除只能由现有提交者操作。

## 贡献者

贡献者不具备直接管理插件本身的额外权限。他们_无法_请求更改插件在目录中的状态。

在论坛中，他们被标记为“插件贡献者”，并有权将涉及他们插件的帖子标记为已解决。

贡献者会在插件的“贡献者与开发者”部分公开列出，并且该插件会显示在用户 WordPress.org 个人资料的插件列表中。

要成为贡献者，用户必须被列入插件 `readme.txt` 文件中的_贡献者_部分。

虽然通常会将参与插件构思或原始贡献的人员添加为贡献者，但您无需为插件添加任何超出您舒适范围的访问权限的人员。

# 接管现有插件

人们因各种原因停止开发自己的插件。与其让这些插件停滞不前，我们鼓励人们将其列出，供其他更活跃的开发者接手。

在接管插件时，您承诺将负责所有未来的开发工作，并确保插件（及您本人）遵守所有[插件指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)。

[alert]并非所有请求都会获得批准，即使审核通过也是如此。[/alert]

## 插件接管流程

有两种方式可以接管插件。

1. 直接联系开发者，对方同意后将您添加为维护者
2. 请求插件审核团队协助接管

既然您正在阅读本文，很可能正在采用第二种方式，请继续阅读下文。您需要完成本文列出的**所有**步骤。

### 1. 检查插件状态

如果插件处于开放和活跃状态，请先自行进行全面审查。确保您对长期维护代码感到舒适且有能力。

如果插件因**未使用**而被关闭，您可以跳过后续步骤，直接发送邮件至 `plugins@wordpress.org`，并附上您提议的插件版本。

如果插件因安全问题被关闭，我们要求**所有**这些问题都必须得到解决。因此，请全力以赴，证明您有动力发现并修复这些问题。

已关闭的插件被接管的可能性**较低**，因为其关闭原因可能更为复杂和棘手。例如，如果插件因许可问题被关闭，我们可能无法为除许可持有者之外的任何人重新开放它。

较大的插件（10万用户或更多）被接管的可能性也较低，因为其用户基数不容小觑，我们需要确保您确实有能力管理如此规模的插件。

### 2. 联系原始开发者

您**必须**尝试联系原始开发者，因为他们可以[授予您插件访问权限](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/#plugin-ownership)。我们建议尝试：

- 发送电子邮件
- 在插件支持页面留言
- 在 GitHub 上提交 issue

我们希望您尽一切合理努力联系他们。如果插件页面显示该插件没有活跃的开发者，那么您可以直接进行下一步。

如果您**确实**联系到了开发者，请要求他们[将所有权转移给您](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/#plugin-ownership)。他们可以自行完成此操作，完成后您即可管理该插件。如果他们遇到问题，请让他们通过电子邮件联系插件团队，我们将提供协助。

如果无法联系到开发者，或者他们没有回复，请继续执行步骤 3。

### 3. 更新代码

即使插件是由开发者直接提供给你的，你也必须从上到下审查代码，以确保其安全、可靠，并符合我们当前的[指南](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)。

你的更新必须包括编辑 readme 文件，以记录新的所有权（最好注明生效时间），移除所有指向原开发者网站/支持资源的链接，并更新版权信息以包含你。请记住，版权是累积的。你需要保留原有的版权信息，并添加你自己的。

如果你的插件是一次重大升级，你_必须_提供升级路径。仅仅想要一个名称/别名并不足以成为接管插件的充分理由。我们非常重视我们的用户，务必不惜一切代价避免因你的升级破坏他们现有网站而损害他们对我们的信任。

请记住，你需要_增加_版本号，以便用户收到升级到新版本的提示。

### 4. 提交代码以供审核

如果仍无法联系到原开发者，您需要向插件审核团队寻求帮助。

完成代码更新后，请发送邮件至 `plugins@wordpress.org`，说明您尝试联系原开发者的方式，并附上打包为 zip 的代码。若无法通过邮件发送 zip 文件，可将代码上传至文件服务（如 Google Drive、Dropbox 等）或提供代码仓库链接（如 Github、Bitbucket 等）。请确保链接是公开的！

收到您提交的 zip 格式代码后，我们将进行审核和测试。此时，您将进入一个**常规**审核流程。也就是说，我们会像对待任何新插件一样，对整个插件的安全性和规范进行全面检查。即使这些问题在原插件中就已存在，您也需要修复它们。

在此阶段，部分插件可能被确认存在安全漏洞。根据问题的性质，我们可能会关闭这些插件，同时您需要承诺不公开披露这些问题。

### 5. 联系原开发者

当我们认为代码符合要求，且您有能力以符合我们指导方针的安全方式持续维护该特定插件时，我们将代表您联系原开发者，提供您的信息并说明您希望接手开发。

我们会尽力确保原插件作者收到通知，但有时这确实无法实现。

### 6. 耐心等待

我们给予原开发者 30 天（1 个月）的时间来回复我们的询问。如果他们回复并拒绝请求，我们将尊重他们的决定，并要求您将插件转换为分叉版本。我们尽力像尊重您作为开发者一样尊重他们，并尊重他们对其作品的意愿。

如果他们批准，我们将协助将插件转移到您的账户。

如果他们未回复，而您已走到这一步，我们很可能会将插件转移给您。

### 7. 更新插件

为了*安全地*更新插件，我们将在添加您之前关闭它。然后您需要通过 SVN 进行更新。完成后，我们会再次检查所有内容是否正确并重新开放。该插件现在将归您所有。

## 常见问题

### 旧的评论/支持帖子会被删除吗？

不会。你将继承所有好的和坏的内容。

### 我是否需要保留原开发者？

不需要。你可以（事实上也应该）撤销任何未积极参与插件维护者的提交权限。但根据版权限制，你必须在代码中保留他们的署名。我们建议你仍将其列为贡献者。

### 原开发者已故，这会影响什么吗？

是的，但并非如你所想。你（显然）可以跳过先向他们请求许可的步骤，但我们实际上会联系开发者的同事和团队，询问他们是否愿意继续维护该插件。在某些情况下，开发者会要求我们在其去世后永久关闭他们的插件。我们尊重他们的意愿。

### 为何我的请求被拒绝？

当我们拒绝插件接管申请时，通常出于以下原因：

- 申请开发者不具备我们认定的插件所需经验
- 所申请插件被认定为高风险
- 现有开发者是拥有商标权的公司或法律实体
- 插件存在法律问题导致无法转移
- 申请开发者存在多次违规记录
- 原始开发者要求我们拒绝转移

如果我们认为不宜移交插件，将尽快通知您。

极少数情况下，插件已移交新所有者但尚未部署代码。通常，若被告知特定插件不可接管，往往存在长期历史问题阻碍我们批准接管。此类情况下，我们建议您以分叉形式提交自己的插件版本。

# 将您的插件转移给新所有者

虽然任何插件都可以拥有无限数量的提交者和支持代表，但任何时候插件只有一个官方所有者。这类似于 WordPress 中的文章只能有一个官方文章作者。

## 适用于用户数少于 10,000 的插件

如果您要将插件转移给新所有者，必须执行以下两个步骤。

首先，将新用户添加为插件的**提交者**：

- 访问 `https://wordpress.org/plugins/YOURPLUGIN/advanced`，将他们的用户名添加为提交者
- 更新 `readme.txt` 文件，将他们的用户 ID 添加为作者

接下来，转到“高级”选项卡并向下滚动到“危险区域”。您将看到**转移您的插件**部分。从下拉菜单中选择新所有者，然后单击按钮。

如果没有其他提交者，插件将无法转移，因此您必须先添加提交者。

![转移此插件界面，包含新所有者选择器和“请转移 -插件名称-”按钮](https://i0.wp.com/developer.wordpress.org/files/2020/04/transfer.jpeg?resize=1024%2C558&ssl=1)

## 适用于用户数超过 10,000（或测试版/精选）的插件

为防止滥用，用户量较大的插件以及被官方认定为精选/测试版的插件禁止进行此类更改。

在这种情况下转移插件，您需要从**当前**所有者的邮箱发送电子邮件至 `plugins@wordpress.org`，并提供以下信息：

1. 转移原因的_简要_说明
2. 新所有者的用户 ID
3. 如果适用，说明精选/测试版插件状态的任何变更

大多数请求都会顺利处理，但如果确定某个插件对 WordPress.org 项目至关重要，或有理由认为请求无效（例如，不是从当前所有者的邮箱发送，或无法确认关联的邮箱地址），请求可能会被拒绝或延迟处理。

# 使用论坛

通过免费的插件托管服务，所有插件开发者都可以使用支持论坛和评价功能来管理他们的插件。

开发者在论坛活动时必须遵守[论坛准则](https://wordpress.org/support/guidelines/)，这可能导致对如何管理插件付费版本产生困惑。

## 本准则适用于所有人

插件所有者是插件的最终控制者。然而，如果其他开发者或支持人员违反准则，插件将面临被警告和关闭的风险。论坛或插件团队没有责任管理您的公司/员工。如果支持人员创建傀儡账号，插件开发者将被通知并需采取行动。如果他们不能或不愿约束其同事，我们将停止托管其插件，并可能封禁该公司。

**_所有人_** 都必须遵守准则。绝无例外。

## 目录并非交易市场

插件目录与论坛并非用于销售或支持非托管于此处的产品。若您在我们系统中注册插件的唯一目的是商业推广，请知悉此行为不会获得任何特殊待遇。

## 用户并非客户

WordPress.org 上的用户并非您的客户，应将其视为用户。这意味着您**不能**向他们索要私人联系信息，不能主动登录他们的网站，也不能向他们收取帮助费用。这是为了您的法律保护，我们禁止这些行为。即使存在购买合同，您仍需对登录其网站造成的损害负责。未经购买授权而登录则风险更高。

若对方是客户（即已购买您的付费产品并在免费论坛发帖），您可以且应当提醒他们：您无法在 WordPress.org 论坛提供支持，并附上专业支持服务的链接。同样，您可以告知用户无法在论坛提供免费支持，并引导至您的网站获取完整说明。

## 您可以要求用户在您自己的系统中提交工单

如果您提供的服务需要账户，这一点尤其适用。您与用户之间已存在既定的关系，要求他们为服务支持提交工单是合理且允许的。如果他们是实际付费客户，您也可以要求他们提交工单。

如果您希望提供更高级别的付费支持服务，告知用户已超出免费支持范围是可以接受的。在这种情况下，我们建议您在自述文件中披露此信息，并在网站上设置专门页面详细说明免费帮助的范围。

对于其他所有情况，如果您在免费论坛中帮助他人，我们期望您直接在论坛内提供帮助。

## 您无需提供免费支持

如果您不想提供免费支持，您没有义务这样做。我们强烈建议您在自述文件中明确说明这一点（最好在描述顶部，以免被忽略），我们将支持您的决定。

请注意，因此给您留下差评的评论不会被删除。当然，您可以回复这些评论，解释您不提供支持的原因，并说明您已披露这一事实。

## 允许对高级版本进行评价（在合理范围内）

这可能会令人困惑。

我们认为，只要满足以下条件，用户有权留下包含插件高级版本详情的评价：

1. 插件本身在自述文件或插件设置页面内推销/推广高级版本
2. 评价包含插件功能/可用性的详细信息
3. 评价**不**仅仅是对推销行为的抱怨
4. 评价**不**包含对开发者的骚扰、辱骂、粗俗言论或其他攻击
5. 评价**不**针对未托管在 WordPress.org（全部或部分）的纯高级版插件

我们不会容忍对开发者的滥用或骚扰，正如我们不会容忍对用户一样。也就是说，负面评价本身并不总是带有攻击性。有时人们只是不喜欢你的插件，这没关系。

## 允许公司拥有多个账户（在合理范围内）

这里真正的问题是，用户不喜欢与“MarvelTech100”交流，而更愿意与卡罗尔·丹弗斯对话。没有面孔的自动化机器人很难让人产生联系感。

话虽如此，拥有一个公司账户来管理你的插件是完全合乎逻辑且合理的。但在这种情况下，该账户仅作为插件/主题的**所有者**，**不允许**在论坛上使用。

如果你想拥有两个账户，一个是你的个人账户，另一个是“公司中的你”，这完全没问题。人们只是更喜欢与人交流。

## 禁止共享账户

与此相关，请勿共享账户。这会导致混乱、纠纷，并且当团队中有人违反准则时，很难理清你的身份。这从来不会有好结果。