开发多站点网络

WordPress 核心包含一组函数和钩子,用于控制网络的某些方面或网络中的单个站点。

在本课中,你将了解需要注意的多站点特定函数命名约定,然后深入探讨一些有用的多站点特定函数和钩子。最后,你将学习如何为网络中的单个站点进行开发,以及在哪里可以获取关于多站点函数和钩子的更多信息。

关于命名约定的说明

WordPress 代码库中,关于多站点存在几种不同的命名约定。

WordPress 多站点最初被称为 WordPress MU(或 WordPress 多用户),许多与多站点相关的函数和钩子都使用 wpmu_ 前缀。

此外,一些函数基于旧术语命名,这些旧术语描述了一个“站点”上的多个“博客”。现在这已被更新为描述一个“网络”上的多个“站点”,但一些旧术语仍然存在于某些函数名称中。

有用的函数

当你开发一个支持多站点网络的产品时,了解一些有用的内部函数是值得的。

第一个是 is_multsite 函数。如果启用了多站点,此函数将返回 true,并且可能是与多站点相关的最广泛使用的函数。如果你在 WordPress 代码库中搜索 is_multsite 函数的使用情况,你会看到它在许多地方被使用,要么是在多站点网络上下文中执行特定任务,要么是将功能限制为仅适用于多站点网络。

还有一些常用函数在开发多站点网络的管理界面时非常有用:

  • is_super_admin 可用于检查当前登录用户是否为网络上的网络管理员。
  • is_network_admin 是 is_admin 函数的多站点等效项,用于确定当前请求是否针对网络管理界面。
  • network_admin_url 是 admin_url 函数的多站点等效项,允许你创建相对于网络管理区域的 URL。这对于重定向到网络管理仪表板的不同区域非常有用。

在处理站点内容时,有一些广泛使用的函数。

is_main_site 确定当前站点是否是当前网络的主站点。

接下来是 get_sites 函数,它将返回与请求参数匹配的站点列表。

然后是 switch_to_blog,它允许你切换到网络中的不同站点,restore_current_blog 在你切换到不同站点后恢复当前站点,以及 get_current_blog_id,它返回当前站点的 ID。

使用这些函数,你可以在整个网络中执行操作。

例如,假设你想创建一个函数来更新网络中特定站点上的一个选项。

function update_site_option( $site_id,  $option_name, $option_value ) {
    switch_to_blog( $site_id );
    update_option(  $option_name, $option_value );
    restore_current_blog();
}

但是,如果你想扩展该功能以在所有站点上更新相同的选项,你可以使用 get_sites 函数,并遍历网络中的所有站点。

function update_option_on_all_sites( $option_name, $option_value ) {
    $sites = get_sites();
    foreach ( $sites as $site ) {
        switch_to_blog( $site->blog_id );
        update_option( 'my_option', 'my_value' );
        restore_current_blog();
    }
}

你也可以使用 update_blog_option 函数来更新特定站点上的选项,而无需切换到该站点。

function update_option_on_all_sites( $option_name, $option_value ) {
    $sites = get_sites();
    foreach ( $sites as $site ) {
        update_blog_option( $site->blog_id, $option_name, $option_value );
    }
}

在开发多站点插件时,有 is_network_only_plugin 函数。这是一个特定于插件的函数,用于检查插件头部中的“Network: true”,以确定是否应仅作为网络范围的插件激活。如果你想限制一个插件只能在整个网络上激活,而不能在单个站点上激活,这将非常有用。

有用的钩子

在开发多站点时,还有一些有用的钩子可以使用。

第一个是 network_admin_menu 钩子,它允许你向网络管理仪表板添加菜单项。如果你想向网络管理仪表板添加自定义菜单项,这将非常有用。

第二个是 network_admin_notices 钩子,它允许你向网络管理仪表板添加通知。这可用于向网络管理员显示通知,就像 admin_notices 用于单站点通知一样。

signup_blogform 是一个过滤器,允许你修改新站点的注册表单。你可以使用它向注册表单添加额外的字段。

wp_initialize_site 是一个动作,在创建新站点时触发。如果你想在创建新站点时执行操作,例如为子站点分配自定义顶级域名,这将非常有用。

为网络中的单个站点进行开发

当你在网络中单个站点的范围内渲染任何内容时,WordPress 核心足够智能,能够知道你正在该站点的范围内工作。

这意味着你用来检索信息的任何函数,例如 get_bloginfo、get_option、get_posts 或 get_post_meta,以及你用来添加或更新信息的任何函数,比如 update_option、wp_insert_post 或 update_post_meta,都会针对你当前正在操作的站点获取、添加或更新正确的数据表。

此外,如果你使用像 register_post_typeregister_taxonomy 这样的函数,它们将仅为当前站点注册。

进一步阅读

要查看所有 WordPress 多站点相关函数和钩子的完整列表,请查阅 WordPress 开发者参考中的多站点包类别。

高级多站点管理

掌握了多站点网络管理的基础知识后,是时候了解一些更高级的多站点管理任务了。

让我们深入了解不同的站点状态选项、删除站点时会发生什么、如何将多站点网络中的单个站点导出为独立安装,以及如何将多站点网络安装恢复为单站点安装。

站点状态 (0:25)

网络管理员可以使用一些站点状态选项,了解这些选项很有用。

停用站点会将站点状态更新为已删除,并向访问该站点的任何人显示一条消息。此外,当站点被停用时,有一个 deactivate_blog 操作钩子;当站点被激活时,有一个 activate_blog 操作钩子,可用于在站点激活或停用时运行额外功能。

归档站点会将站点状态更新为已归档,并向访问该站点 URL 的任何人显示一条归档消息。

将站点标记为垃圾会将站点状态更新为垃圾,并显示与归档相同的消息,但不会触发额外的钩子。

从网络中删除站点

从网络中删除站点将移除与该站点关联的所有内容,包括文章、页面、评论以及任何其他自定义内容类型。它还会删除数据库中用于存放站点内容的任何表。与将文章或页面放入回收站不同,一旦删除站点,就无法撤销此操作。

将站点导出为单站点安装

在某些情况下,您可能希望将其中一个子站点提取出来,作为独立的单站点 WordPress 安装。这是可行的,但需要一些手动步骤。有几种方法可以做到这一点,这里只介绍一种可能性。

首先,登录子站点,然后浏览到 工具 > 导出 页面。

您可以使用 WordPress 导出工具将内容导出为 WordPress eXtended RSS 或 WXR 格式。

然后创建新的单站点和关联的用户。

确保安装子站点上使用的任何插件和主题。

然后浏览到工具 -> 导入,使用 WordPress 导入器将数据导入新站点。您可能需要点击 WordPress 选项下的“立即安装”来安装导入器,安装完成后点击“运行导入器”。

别忘了将数据分配给新站点上的相关用户。

数据导入后,手动将子站点的上传目录复制到新的单站点安装中。

您可以在多站点安装的 wp-content/uploads/sites/ 目录中找到子站点的上传目录,该目录位于一个以子站点 ID 命名的文件夹中。

最后但同样重要的是,运行一个搜索替换工具,例如 Better Search Replace,来更新数据库中的任何 URL。

您需要将子站点 URL(子域名或子目录版本)替换为新的单站点 URL。如果您将顶级域名指向了子站点,这一步可能不是必需的。

最后,全面测试一切,确保其按预期工作。

WordPress 数据导出选项的替代方法是手动将子站点的数据库表复制到新站点。但是,如果内容未与正确的用户关联,这可能会导致进一步的问题。

注意事项

如果您有任何创建自定义数据库表的插件,您可能需要手动将这些表复制到新安装中。

在这种情况下,依赖具有多站点扩展的付费第三方备份解决方案可能更容易,这些扩展会为您处理这些操作。

将多站点转换回单站点安装

也可以将多站点转换回单站点安装。如果您不再需要多站点功能,但希望保留原始站点,这将非常有用。

如果您有其他子站点,最好先将它们导出为单站点安装,因为这会删除子站点的内容表。此外,删除为子站点创建的任何用户。

要恢复为单站点,您首先需要移除 wp-config.php 中所有与多站点相关的常量。

完成此操作后,您的仪表盘将恢复为单站点仪表盘。

然后,如果您之前更新了 .htaccess 文件,您需要将其恢复为原始的 .htaccess 文件。

您可以通过将固定链接重置为您想要的任何格式来实现这一点。

最后,删除多站点安装期间专门创建的任何表,即:

wp_blogs
wp_blog_versions
wp_registration_log
wp_signups
wp_site
wp_sitemeta
wp_usermeta
wp_users

如果一切顺利,您现在应该拥有一个正常工作的主站点单站点安装。

进一步阅读

如需查看所有多站点管理功能的完整列表,请查阅 WordPress 开发者手册中“高级管理”下的“多站点”部分。

管理WordPress多站点网络

管理 WordPress 多站点网络与管理单站点 WordPress 安装略有不同。

在本课中,你将学习如何管理 WordPress 多站点网络。

你将了解网络管理仪表板、网络设置页面中的可用选项,以及在网络上创建和管理子站点的不同方法。

网络管理仪表板

一旦启用了多站点网络,你的管理员用户将变为超级管理员,并可以访问网络管理仪表板。在这里,你可以管理网络中的站点、用户、主题、插件和设置。

网络设置页面

网络设置页面是管理多站点网络的地方。

操作设置允许你设置网络标题和管理员邮箱。

注册设置允许你设置网络的注册选项。

例如,如果你想允许用户在网络中注册新站点,可以启用“站点和用户账户都可以注册”选项。

你还可以设置新站点的禁用名称、限制特定电子邮件域名的注册,以及禁止特定电子邮件域名的注册。

新站点设置控制着在网络中创建新站点时发送的电子邮件和创建的内容。

上传设置控制文件上传,包括文件大小限制和允许的文件类型。

语言设置允许你控制网络中新建站点的默认语言。

最后,菜单设置允许你控制站点管理员可以访问哪些菜单。

创建站点

你可能想做的第一件事就是创建一个子站点。为此,请转到站点页面,并点击“添加新站点”按钮。系统会要求你输入站点地址(子域名或子目录)、站点标题、站点语言和站点管理员邮箱地址。如果你使用现有用户的邮箱地址,该用户将被添加为站点管理员。否则,将创建一个新用户并设为新站点的管理员。

站点创建后,你可以通过点击站点列表中的“编辑”按钮来编辑该站点。

信息选项卡管理站点地址,显示站点的注册时间和最后更新时间,并管理站点属性。

用户选项卡允许你将现有用户添加到站点,或向站点添加新用户。

主题选项卡允许你为站点激活一个主题。这里可用的主题是那些未在整个网络中激活的主题。这意味着你可以为网络中的特定站点设置专属主题。

设置选项卡包含你可以管理的所有其他站点设置。

为站点分配顶级域名

还可以为网络中的站点设置顶级域名或根域名。如果你或站点管理员想为网络中的某个站点使用不同的域名,这将非常有用。

要实现这一点,你需要在域名注册商处注册该域名,并将其指向与多站点安装相同的位置。

在这种情况下,你可以看到新的顶级域名指向与多站点安装相同的位置,因为它会重定向到多站点的 URL。

然后,在网络管理中,编辑相关站点,并将站点地址字段更改为新的顶级域名。

现在,当你浏览该域名时,将被重定向到网络中关联的站点。

允许用户注册自己的站点

根据你计划如何使用多站点网络,你可能希望用户能够注册自己的子站点。为此,请转到网络设置页面,并启用“站点和用户账户都可以注册”选项。

现在,如果用户浏览默认的 WordPress 注册 URL,他们将看到还可以在网络中注册站点的选项。

// 示例代码块,内容保持不变

输入用户名和邮箱地址后,系统会询问他们是想要创建站点,还是仅在网络中注册用户账户。

如果他们选择创建站点,则需要设置站点名称和标题。

一旦他们点击注册,站点将被创建,用户将收到一封激活新站点的电子邮件,之后他们就可以登录到该子站点。

进一步阅读

有关管理多站点网络的更多详细信息,请查看 WordPress 开发者文档中的网络管理和网络设置页面。

搭建WordPress多站点网络

WordPress 多站点网络是一种在单个 WordPress 安装中托管多个 WordPress 站点的方式。

在本课中,你将进一步了解 WordPress 多站点、为何要考虑设置多站点网络,以及创建多站点网络需要遵循的步骤。

什么是 WordPress 多站点网络?

WordPress 多站点网络是一组共享单个 WordPress 安装的 WordPress 站点。通过多站点网络,可以允许用户创建自己的站点,或者将网络配置为仅允许管理员创建站点。这些站点在网络中被称为“子站点”或“独立站点”。

多站点网络中的所有站点都使用相同的 WordPress 安装文件,并共享相同的插件和主题。插件和主题在网络级别安装,然后在各个独立站点上激活。

每个独立站点在共享安装中都有独立的媒体上传目录,并在数据库中有独立的表来存储站点内容。

需要注意的是,虽然它们共享相同的 WordPress 核心文件,但多站点网络中的各个站点是相互独立的。它们不像其他类型的网络那样相互连接。如果你计划创建紧密互联、共享数据或共享用户的站点,那么多站点网络可能不是最佳解决方案。

为何要使用 WordPress 多站点网络?

当你拥有多个性质相似但需要相互独立的站点时,多站点网络是一个很好的解决方案。

这方面的例子包括高等教育网站、非营利组织和开源项目。

一个活跃多站点网络最典型的例子就是 Make WordPress。

在这里,每个参与 WordPress 开源项目的贡献者团队都拥有自己的子站点,这些子站点是 Make WordPress 网络的一部分。这意味着,如果你是核心团队的成员,你只能登录到核心子站点,并为该团队创建内容。

多站点网络支持

你使用的网络主机或本地开发环境将决定你如何在本地或在线服务器上创建多站点网络。两种常见的选择是:要么在新安装过程中提供一个设置选项,将新安装设置为多站点网络;要么在 WordPress 安装完成后,按照手动步骤设置多站点网络。

使用 Apache 网络服务器的网络主机和本地开发环境通常允许你使用手动方法将现有的 WordPress 安装转换为多站点网络。使用 Nginx 的环境通常要求你在创建新站点的过程中创建多站点网络。这是因为默认情况下,nginx 不支持用于手动启用多站点的 .htaccess 文件。

无论你使用的是自动还是手动启用多站点的网络主机或本地开发环境,了解从 WordPress 安装创建多站点网络所需的额外步骤都是一个好主意,这也是本课的重点。

如何创建多站点网络

在创建多站点网络之前,请务必阅读开发者文档中的“在创建网络之前”页面,因为它涵盖了一些你在创建多站点网络之前需要了解的重要注意事项。

接下来,你应该备份当前站点的文件和数据库。这不是绝对必要的,特别是如果你创建了一个全新的 WordPress 安装,但如果你已经在要转换为多站点网络的站点上创建了一些内容,那么备份是个好主意。

此外,如果你有站点文件的备份,那么在创建多站点网络过程中如果出现任何问题,你可以快速撤销所做的任何更改。

同时,你应该确保站点上的固定链接正常工作,并且已停用所有已安装的插件。

最后,如果你希望将 WordPress 安装运行在其自己的目录中,请确保在创建多站点网络之前完成此操作。

启用多站点

要启用多站点,你需要编辑 WordPress 安装根目录下的 wp-config.php 文件,并将 PHP 常量 WP_ALLOW_MULTISITE 定义为 true

/* 多站点 */
define( 'WP_ALLOW_MULTISITE', true );

然后,刷新你的 WordPress 仪表盘。通过设置此常量,你现在会在仪表盘的“工具”菜单下看到一个新的菜单项,名为“网络设置”。

安装网络

“网络设置”页面将引导你完成创建多站点网络的步骤。

首先,你需要选择是使用顶级域名的子域名还是子目录作为网络上站点的地址。子目录更简单,因为你不需要任何额外的 DNS 配置,但子域名能让站点拥有更专业的 URL。要使用子域名,你需要为你的顶级域名设置一个通配符 DNS 记录。

在本课中,我们将使用子目录。

您可以保留其余设置不变,或根据自己的喜好进行更改。

准备好后,点击“安装”按钮。

WordPress 现在将运行网络安装,创建所需的任何数据库表,并向这些表添加所需的特定数据。

启用网络

安装完成后,您需要启用网络。这需要做两处更改:

  1. 再次编辑 wp-config.php 文件,并添加仪表盘中“启用网络”页面第一步描述的常量。
  2. 编辑 WordPress 安装根目录中的 .htaccess 文件,并添加仪表盘中“启用网络”页面第二步描述的规则。

完成这些更改后,刷新您的 WordPress 仪表盘,系统会要求您重新登录。

您将被重定向回“网络设置”页面,但会注意到您现在已作为网络管理员登录,并且新创建的多站点网络为您提供了一组不同的菜单选项。

进一步阅读

有关如何创建多站点网络的更多详细信息,请务必阅读官方 WordPress 文档中的“创建网络”页面。

常用的国际化函数

要开始对代码进行国际化,您需要了解 WordPress 提供的用于帮助您完成此过程的函数。

在本课中,您将学习 WordPress 中最常用的国际化函数,以及如何在代码中使用它们。

如何对代码进行国际化 (0:18)

每当您编写一段将显示给用户的文本字符串时,都应使用 WordPress 的 i18n 函数,以确保它可以被翻译。

有许多可用的 i18n 函数,每个函数都执行与国际化相关的不同任务。

最常用的 i18n 函数是 __() 函数。此函数接收一个文本字符串,并返回该字符串的翻译版本。如果没有可用的翻译,则返回原始字符串。

您还会注意到,此函数以及大多数其他 i18n 函数都接受第二个参数。此参数用于指定文本域。

文本域是您的插件或主题的唯一标识符。它用于确保使用正确的翻译。

主题和插件在其头部都有一个“文本域”字段。这用于指定主题或插件的文本域。

每当您使用翻译函数时,都应始终将文本域作为第二个参数包含在内。

__( 'Some Text', 'my-textdomain' );

要了解其工作原理,让我们看一个示例:

首先,浏览到 GitHub 上的“初学者开发者学习路径”仓库,并下载“国际化主题”zip 文件。

然后,在您的 WordPress 网站上安装该主题,并在代码编辑器中浏览它。

functions.php 文件中,一个 JavaScript 文件在 WordPress 仪表盘的上下文中被入队。它还在“外观”菜单中注册了一个子菜单项,用于渲染主题设置页面。

<?php
/**
 * Enqueue theme.js file in the dashboard.
 */
add_action( 'admin_enqueue_scripts', 'internationalization_enqueue_scripts' );
function internationalization_enqueue_scripts() {
    wp_enqueue_script(
        'internationalization-theme-js',
        get_stylesheet_directory_uri() . '/assets/theme.js',
        array(),
        '1.0.0',
        true
    );
}

/**
 * Create a submenu item under the "Appearance" menu.
 */
add_action( 'admin_menu', 'internationalization_add_submenu_page' );
function internationalization_add_submenu_page() {
    add_submenu_page(
        'themes.php',
        'Internationalization',
        'Internationalization',
        'manage_options',
        'internationalization',
        'internationalization_display_page'
    );
}

/**
 * Render the page for the submenu item.
 */
function internationalization_display_page() {
    ?>
    <div class="wrap">
        <h1>Internationalization Settings</h1>
        <p>This is a settings page for the Internationalization theme</p>
        <button id="internationalization-settings-button" class="button button-primary">Alert</button>
    </div>
    <?php
}

如果您浏览到设置页面,它会包含一个按钮,点击该按钮会显示一个警告框。

此警告框由主题的 JavaScript 文件处理。

/**
 * Add event listener to the button
 */
document.querySelector( '#internationalization-settings-button' ).addEventListener( 'click', function(){
    alert( 'Settings button clicked' );
} );

在这段代码中,您有许多需要国际化的英文字符串。

第一步是国际化 PHP 代码中的文本字符串。为此,您可以将文本字符串包裹在 __() 函数中,并指定文本域。

首先检查主题的文本域。对于主题,这位于 style.css 文件中;对于插件,则位于主插件文件中。

在这种情况下,文本域是 internationalization。如果此主题没有文本域,您需要指定一个。

然后,更新 internationalization_add_submenu_page() 函数中的文本字符串。

/**
 * Create an admin submenu item under the "Appearance" menu.
 */
add_action( 'admin_menu', 'internationalization_add_submenu_page' );
function internationalization_add_submenu_page() {
    add_submenu_page(
        'themes.php',
        __( 'Internationalization', 'internationalization' ),
        __( 'Internationalization', 'internationalization' ),
        'manage_options',
        'internationalization',
        'internationalization_display_page'
    );
}

您可以为 internationalization_display_page() 函数中的文本字符串执行相同操作。

/**
 * Render the page for the submenu item.
 */
function internationalization_display_page() {
    ?>
    <div class="wrap">
        <h1><?php echo __( 'Internationalization Settings', 'internationalization' ); ?></h1>
        <p><?php echo __( 'This is a settings page for the Internationalization theme', 'internationalization' ); ?></p>
        <button id="internationalization-settings-button" class="button button-primary"><?php echo __( 'Alert', 'internationalization' ); ?></button>
    </div>
    <?php
}

WordPress 还包含一个用于直接输出可翻译字符串的简写函数。此函数是 _e() 函数。它既进行国际化,又直接输出字符串。您可以使用此函数来简化代码。

/**
 * Render the page for the submenu item.
 */
function internationalization_display_page() {
    ?>
    <div class="wrap">
        <h1><?php _e( 'Internationalization Settings', 'internationalization' ); ?></h1>
        <p><?php _e( 'This is a settings page for the Internationalization theme', 'internationalization' ); ?></p>
        <button id="internationalization-settings-button" class="button button-primary"><?php echo __( 'Alert', 'internationalization' ); ?></button>
    </div>
    <?php
}

接下来,您需要国际化 JavaScript 文件中的文本字符串。

为此,有一个与 PHP 的 __() 函数等效的 JavaScript 版本,它在 WordPress 前端的 wp.i18n 对象中可用。

为了确保您可以使用此函数,您需要更新您的 internationalization_enqueue_scripts() 函数,以将 wp-i18n 包作为依赖项。这将确保您的 JavaScript 代码仅在 wp-i18n 包加载且 wp.i18n 对象可用时才会被加载。

/**
 * Enqueue theme.js file in the dashboard.
 */
add_action( 'admin_enqueue_scripts', 'internationalization_enqueue_scripts' );
function internationalization_enqueue_scripts() {
    wp_enqueue_script(
        'internationalization-theme-js',
        get_stylesheet_directory_uri() . '/assets/theme.js',
        array( 'wp-i18n' ),
        '1.0.0',
        true
    );
}

然后,您需要为您想要翻译的脚本调用 wp_set_script_translations 函数。此函数接受脚本的句柄和文本域作为参数。这将加载脚本的翻译。

wp_set_script_translations( 'internationalization-theme-js', 'internationalization' );

完成此操作后,您就可以设置并使用来自 wp.i18n 对象的 __() 函数来翻译 JavaScript 文件中的文本字符串。

const __ = wp.i18n.__;
/**
 * Add event listener to the button
 */
document.querySelector( '#internationalization-settings-button' ).addEventListener( 'click', function(){
    alert( wp.i18n.__( 'Settings button clicked', 'internationalization' ) );
} );

如果您刷新页面,不会立即看到任何变化,所有功能仍将按预期工作。

但是,如果有人愿意,他们可以基于所有国际化的文本字符串为主题生成英文语言文件,以便进行翻译。

块开发中的国际化函数

如果您正在为块编辑器开发一个块,您也可以在块的 JavaScript 中使用 JavaScript 翻译函数来国际化块中的文本字符串。

您需要做的就是从 @wordpress/i18n 包中导入相关函数。

/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
 */
import { __ } from '@wordpress/i18n';

进一步阅读

本课程仅涵盖部分可用的国际化函数。如需了解所有可用函数的更多信息,请查阅 WordPress 开发者资源中通用 API 手册的国际化章节。

什么是国际化?

国际化是指开发软件时,使其能够在不修改源代码的情况下轻松翻译成其他语言的过程。

让我们了解在 WordPress 中什么是国际化、为什么它很重要,并学习在哪里可以找到更多关于如何在 WordPress 开发中实现国际化的信息。

什么是国际化?

WordPress 被全球使用,用户使用多种语言。

因此,WordPress 中的任何文本字符串都需要进行编码,以便它们可以轻松翻译成其他语言。

让我们看一个例子:

当你第一次登录 WordPress 仪表盘时,页面顶部会出现“仪表盘”这个标题。

你可以在 wp-admin/index.php 文件的第 33 行找到生成这个标题的代码。

$title       = __( 'Dashboard' );

这里 $title 变量被设置为“仪表盘”。

注意文本字符串是如何被包裹在 __() 函数中的。这是一个 WordPress 函数,用于使文本字符串可翻译。

如果你访问用户的个人资料并将其更新为另一种语言,文本字符串将被翻译成该语言。

例如,如果我将我的用户个人资料更改为西班牙语,标题将被翻译为“Escritorio”。

但底层的代码保持不变。

这是可能的,因为西班牙语的语言文件已安装在网站上,并且包含了文本字符串“仪表盘”的翻译。

#. translators: Network menu item.
#: wp-includes/admin-bar.php:431 wp-includes/admin-bar.php:596
#: wp-includes/admin-bar.php:716 wp-includes/deprecated.php:2822
#: wp-includes/deprecated.php:2824 wp-admin/index.php:33 wp-admin/menu.php:24
#: wp-admin/my-sites.php:142 wp-admin/user/menu.php:10
#: wp-admin/includes/class-wp-ms-sites-list-table.php:746
#: wp-admin/network/index.php:21 wp-admin/network/menu.php:11
#: wp-admin/network/site-info.php:138 wp-admin/network/site-settings.php:95
#: wp-admin/network/site-themes.php:181 wp-admin/network/site-users.php:226
msgid "Dashboard"
msgstr "Escritorio"

由于使用了翻译函数,一旦为用户设置了语言,WordPress 将在相关语言文件中搜索该词的翻译,并根据上下文显示或存储翻译。

国际化是编写代码的过程,通过将可能显示给用户的任何文本字符串包裹在正确的翻译函数中,使其可翻译。

国际化通常缩写为 i18n(因为字母 i 和 n 之间有 18 个字母)。

将国际化的文本字符串翻译并适应特定区域或语言的过程称为本地化。

虽然本地化不在本课程范围内,但你可以在 WordPress 开发者资源中通用 API 手册的本地化部分阅读更多相关内容。

国际化不是什么

国际化并不等同于确保你网站前端的内容以多种语言提供。

这通常被称为确保你的内容是多语言或已翻译的。

由于这些内容存储在数据库中,最好为每种你想要支持的语言准备一个完全翻译的网站副本。

这可以通过使用 TranslatePress、Polylang 或 WeGlot 等插件来实现。

在哪里找到更多信息

WordPress 开发者资源在通用 API 手册中有一个关于国际化的优秀部分。它包括一个过程概述,以及一个列出 WordPress 中常用于国际化的函数的页面。它还包含一个关于国际化指南的部分,对于任何希望使其 WordPress 插件或主题可翻译的开发者来说,这都是必读内容。

安全漏洞检测工具

为了帮助你开发安全的 WordPress 插件和主题,有许多工具可以帮助你检测代码中的安全漏洞。

在本课中,你将了解一些可用于测试代码安全漏洞的工具,并简要介绍如何使用它们。

你还将了解在哪里可以找到关于 Web 应用程序关键安全风险的更多信息。

插件

WordPress.org 仓库中有两个插件可以帮助你验证代码。

Plugin Check 是一个 WordPress 插件,你可以用它来测试你的插件是否符合 WordPress.org 插件目录的要求标准,其中一项要求就是你的插件代码是安全的。

安装并激活该插件后,你可以从 WordPress 管理仪表盘的“工具”菜单中访问 Plugin Check 管理页面。

选择你要测试的插件,并确保勾选了“安全”复选框。

点击检查!按钮运行测试。

结果会以表格形式显示,列出需要解决的问题,包括文件名和行号。

还有一个 Theme Check 插件,它与 Plugin Check 类似,但适用于主题。

安装此插件后,你可以从 WordPress 管理仪表盘的“外观”菜单中访问 Theme Check 管理页面。

同样,你选择要检查的主题,然后点击检查!按钮运行测试。

结果会显示在页面底部,列出需要解决的问题。

命令行

如果你想要可以从命令行运行的工具,可以使用 PHP_CodeSniffer 配合 WordPress 编码标准规则。

这种组合不仅会根据 WordPress 编码标准检查你的代码,还会检查安全漏洞。

要使用 PHP_CodeSniffer 配合 WordPress 编码标准,你需要安装 Composer,这是一个用于 PHP 项目的依赖管理器。

安装 Composer 不在本课的讨论范围内,但你可以在 Composer 网站上找到适用于 macOS/Linux 和 Windows 操作系统的安装说明。

为了让 Composer 工作,你还需要在计算机上安装 PHP,这样你就可以使用 PHP CLI 二进制文件,它允许你在终端中运行 PHP 脚本,而不仅仅是在浏览器中。

你可以在 PHP 手册的“安装与配置”部分找到在你的系统上安装 PHP 的方法。

安装 PHP 后,请确保将 PHP CLI 二进制文件的路径添加到计算机的操作系统路径中,这样你就可以在计算机的任何位置运行 PHP 命令。

要测试这一点,你可以在终端中运行以下命令:

php -v

它应该会输出 PHP 版本。

安装 Composer 后,你在终端中运行以下命令,在你的插件或主题目录中初始化一个新的 Composer 项目:

composer init

按照终端内的说明创建一个新的 Composer 项目。

这将在当前目录中创建一个 composer.json 文件,其中包含你项目的依赖项列表。

接下来,按照 WordPress 编码标准规则仓库中的说明,在你的插件目录中安装所有必需的依赖项:

composer require --dev wp-coding-standards/wpcs:"^3.0"

安装完成后,你可以使用 WordPress 标准对代码运行 PHP_CodeSniffer,它会输出需要解决的问题列表。

vendor/bin/phpcs --standard=WordPress your-plugin-file.php

代码编辑器

根据你使用的代码编辑器,有多种方法可以在编码时检查漏洞。

例如,Visual Studio Code 有许多扩展可以在编辑器内运行 PHP_CodeSniffer 工具,你可以在 VS Code 扩展市场中搜索“phpcs”找到它们。

此外,还有一些第三方服务可以在你的代码编辑器中扫描代码漏洞,例如 Sonar 的 SonarLint 工具。

SonarLint 对所有开源项目完全免费。只有当你想要分析私有仓库时才需要付费。

SonarLint 可作为 Visual Studio Code、JetBrains 编辑器和 Eclipse 的插件使用。

配置正确后,这些扩展可以在你编写代码时高亮显示问题,这样你就可以在提交代码之前修复它们。

OWASP

虽然不特指某个工具,但熟悉开放 Web 应用程序安全项目(OWASP)也是一个好主意。

OWASP 是一个致力于提高软件安全性的非营利基金会。他们提供了许多资源,包括 OWASP Top 10,这是 Web 应用程序最关键的十大安全风险列表。

安全是一个不断变化的领域,漏洞也会随着时间的推移而演变。通过遵循开放网络应用安全项目(OWASP)十大列表,你可以及时了解最新的安全风险和最佳实践。

修复常见安全漏洞

在本模块的第一课中,您学习了开发自定义插件或主题时应遵循的主要安全原则。

在本课中,您将学习如何在开发 WordPress 插件和主题时应用这些原则,通过修复一个编码不安全的表单提交插件来实践。

编码糟糕的表单提交插件

首先,下载 Learn Plugin Security 插件,并将其安装到您的本地开发环境中。

然后,在代码编辑器中打开主插件文件,查看代码。

  1. 在主插件 PHP 文件的顶部,设置了一些常量,这些常量在插件其他地方使用。前两个用于定义插件将用于重定向的页面别名。为了使插件功能正常工作,这些页面必须存在,且具有正确的别名。
  2. 接下来,在插件激活钩子上注册了一个回调函数。这会在数据库中创建一个自定义的 form_submissions 表。
  3. 之后,插件的管理 JavaScript 和前端样式 CSS 文件被加载。
  4. 接着,注册了一个短代码,用于在前端显示表单。
  5. 然后,一个回调函数被挂载到 wp 动作上,插件使用它来处理表单提交。
  6. 接下来,插件注册了一个管理子菜单,用于显示表单提交列表。
  7. 有一个函数,管理子菜单使用它从数据库中获取表单提交数据。
  8. 最后,有一个回调函数被挂载到 wp_ajax 函数上,这是插件用于从管理子菜单页面删除表单提交的 Ajax 端点。

管理 JavaScript 文件处理从提交页面删除表单提交的 Ajax 请求。

前端 style.css 文件在用户为短代码输入 class 属性时使用。默认颜色为红色,但用户可以将其更改为蓝色。它只是为表单添加了一个边框。

当短代码被添加到文章或页面时,它会渲染表单,用户可以提交他们的详细信息。表单提交后,会根据提交是否成功重定向到成功或错误页面。然后在仪表盘中,管理员可以查看表单提交,并使用 Ajax 删除提交。

SQL 注入

我们要查找的第一个常见漏洞是 SQL 注入。

当输入的值没有被正确清理,允许使用输入数据的任何 SQL 命令可能在数据库上执行时,就会发生 SQL 注入。

我们需要解决潜在 SQL 注入漏洞的第一个地方是在 wp_learn_maybe_process_form 函数中。

function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }
    $name = $_POST['name'];
    $email = $_POST['email'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
    $result = $wpdb->query($sql);
    if ( 0 < $result ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}
  1. 我们需要确保在查询中使用任何 $_POST 数据之前,先对其进行清理。
  2. 我们需要使用 wpdb 的 prepare 或 insert 函数。
function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }
    $name = sanitize_text_field($_POST['name']);
    $email = sanitize_email($_POST['email']);

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows = $wpdb->insert(
        $table_name,
        array(
            'name' => $name,
            'email' => $email,
        )
    );
    if ( 0 < $rows ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}

这将确保名称和电子邮件字段值在从表单提交请求中接收时都被清理,并且在用于将记录存储到数据库之前也被清理。虽然这看起来可能有些过度,但如果您只清理输入,而代码后来被更改为以不同方式使用这些值,您仍然可能容易受到 SQL 注入攻击。

另一个需要防止 SQL 注入的地方是在 wp_learn_delete_form_submission 函数中。

  1. 我们需要确保在查询中使用任何 $_POST 数据之前,先对其进行清理。
  2. 我们需要使用 wpdb 的 prepare 或 delete 函数。
function wp_learn_delete_form_submission() {
    $id = (int) $_POST['id'];
    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows_deleted = $wpdb->delete( $table_name, array( 'id' => $id ) );
    if ( 0 < $rows_deleted ) {
        $result = 'success';
    } else {
        $result = 'error';
    }
    return wp_send_json( array( 'result' => $result ) );
}

由于这是一个整数,我们可以使用 PHP 类型转换功能来确保它始终被转换为整数。

跨站脚本攻击 (XSS) (3:47)

我们要查找的下一个常见漏洞是跨站脚本攻击 (XSS)。

当恶意方将 JavaScript 注入到网页中时,就会发生跨站脚本攻击 (XSS),这可用于从网站发起多种不同的攻击或恶意活动。

您可以通过转义输出、剥离不需要的数据来避免 XSS 漏洞。您的代码应根据要转义的内容类型,使用适当的函数转义动态内容。

让我们看看一些输出数据的地方,并确保它们被正确转义。

第一个地方是在 wp_learn_form_shortcode 短代码回调的包装 div 中。

<div id="wp_learn_form" class="<?php echo $atts['class'] ?>">

在这里,div 的 class 属性是根据传递给短代码的属性渲染的。这是一个潜在的 XSS 漏洞,因为 class 属性没有被转义。

<div id="wp_learn_form" class="<?php echo esc_attr( $atts['class'] ) ?>">

请注意,您应该专门使用 esc_attr 函数来转义 HTML 属性。

接下来,我们有 wp_learn_render_admin_page 函数,它用于渲染管理页面。

function wp_learn_render_admin_page(){
    $submissions = wp_learn_get_form_submissions();
    ?>
    <div class="wrap" id="wp_learn_admin">
        <h1>Admin</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <?php foreach ($submissions as $submission){ ?>
                <tr>
                    <td><?php echo $submission->name?></td>
                    <td><?php echo $submission->email?></td>
                    <td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
                </tr>
            <?php } ?>
        </table>
    </div>
    <?php
}

这里,$submission->name、$submission->email 和 $submission->id 应该被转义。

<?php foreach ($submissions as $submission){ ?>
    <tr>
        <td><?php echo esc_html($submission->name)?></td>
        <td><?php echo esc_html($submission->email)?></td>
        <td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>
    </tr>
<?php } ?>

在这个示例中,你可以使用 esc_html,因为当 HTML 元素包含要显示的数据段时,这是正确的函数。务必密切关注每个转义函数的作用,因为有些会移除 HTML,而有些则允许保留。

你必须根据要输出的内容和上下文,选择最合适的函数。

最后,你将 ID 转换为整数,因为它被用于数据属性中。

跨站请求伪造 (CSRF)

下一个需要防范的漏洞是跨站请求伪造。CSRF 是指恶意方诱骗用户在他们已认证的 Web 应用中执行非预期的操作。

在使用 WordPress 开发时,熟悉 WordPress nonce 是帮助防范 CSRF 的必备技能。

Nonce 是“一次性使用的数字”,它提供了一种验证请求来源是否合法的方式。

  1. 当你需要验证请求是否合法时,创建一个 nonce。
  2. 你将 nonce 输出或传递给需要发起请求的地方。
  3. 当请求发起时,你验证该 nonce。

这个插件中存在两个可能的 CSRF 漏洞。

第一个漏洞出现在表单提交并处理数据时。要修复此问题,我们需要在短代码渲染的表单中添加一个 nonce,然后在表单提交时验证它。

在表单中,我们使用 wp_nonce_field 函数来添加一个包含 nonce 的隐藏字段。

<?php
wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );
?>

注意你是如何传入一个 action 和一个 name 的。action 用于标识 nonce,而 name 是添加到表单中的字段名称。

如果你检查表单,可以看到 nonce 字段,它使用了你传递给函数的名称以及 nonce 值。

然后在表单提交函数中,你使用 wp_verify_nonce 函数来验证 nonce,传入 nonce 字段的值和 action。

    /**
     * 04 (b). Verify the nonce
     * https://developer.wordpress.org/apis/security/nonces/
     */
    if ( ! isset( $_POST['wp_learn_form_nonce_field'] ) || ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) ) {
        wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
        die();
    }

这里我们检查 nonce 字段是否已在请求中传递,然后验证 nonce 是否有效。如果 nonce 未传递或无效,我们将重定向到错误页面。

另一个需要防范 CSRF 的地方是用于删除表单提交数据的 Ajax 回调。

function wp_learn_delete_form_submission() {
    $id = (int) $_POST['id'];
    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows_deleted = $wpdb->delete( $table_name, array( 'id' => $id ) );
    if ( 0 < $rows_deleted ) {
        $result = 'success';
    } else {
        $result = 'error';
    }
    return wp_send_json( array( 'result' => $result ) );
}

要修复此问题,首先我们需要使用 wp_create_nonce 手动创建一个 nonce,然后使用 wp_localize_script 将 nonce 传递给 JavaScript 层。

  /**
   * 04 (a). Add an ajax nonce to the script
   * https://developer.wordpress.org/apis/security/nonces/
   */
  $ajax_nonce = wp_create_nonce( 'wp_learn_ajax_nonce' );
  wp_localize_script(
      'wp-learn-admin',
      'wp_learn_ajax',
      array(
          'ajax_url' => admin_url( 'admin-ajax.php' ),
          'nonce'    => $ajax_nonce,
      )
  );

然后,我们需要在 jQuery POST 请求中包含该 nonce。

jQuery.post(
    wp_learn_ajax.ajax_url,
    {
        '_ajax_nonce': wp_learn_ajax.nonce,
        'action': 'delete_form_submission',
        'id': id,
    },
    function (response) {
        console.log( response );
        alert( 'Form submission deleted' );
        document.location.reload();
    }
);

注意我们如何在 POST 请求中将 nonce 指定为 _ajax_nonce。这是 WordPress 在处理 Ajax 请求时预期的 nonce 名称。

最后,在 Ajax 回调中,我们使用方便的 check_ajax_referrer 函数来验证 nonce。

    check_ajax_referer( 'wp_learn_ajax_nonce' );

你会看到传递给 check_ajax_referer 的字符串与我们创建 nonce 时传递给 wp_create_nonce 的字符串相同。

如果 check_ajax_referrer 失败,它将导致执行停止,因此我们不需要检查该函数的结果。

访问控制失效

这个插件中还有一个漏洞,即访问控制失效。BAC 是指用户能够访问他们本不应访问的资源。例如,用户可能能够访问管理员功能,即使他们不是管理员。

在我们的示例中,Ajax 函数存在一个访问控制失效漏洞。目前,任何人都可以使用正确的数据向 Ajax 请求 URL 发起请求,从而删除一个 form_submission。

要修复此问题,我们可以使用 WordPress 角色和权限 API 来检查用户是否具有删除表单提交数据的正确权限。在这种情况下,只需检查用户是否为管理员用户即可。

    if ( ! current_user_can( 'manage_options' ) ) {
        return wp_send_json( array( 'result' => 'Authentication error' ) );
    }

注意我们在这里进行了两次检查,一次针对 CSRF,一次针对访问控制。在这个示例中,执行顺序不是特别重要,但通常来说,先检查 CSRF,再检查访问控制是一个好习惯。

附加题 – 开放重定向

这个插件中还有一个额外的安全漏洞。你能找到它吗?

这是一个很难发现的漏洞,但所有使用 wp_redirect 的地方都应替换为 wp_safe_redirect。这是因为代码正在重定向到本地 URL,而 wp_safe_redirect 会检查其使用的 $location 在具有绝对路径时是否为允许的主机。这可以防止如果重定向 $location 受到攻击时可能发生的恶意重定向。

进一步阅读

要了解更多关于修复 WordPress 代码中常见漏洞的信息,请务必收藏 WordPress 开发者文档安全部分中关于常见漏洞的页面,以及包含权限检查、数据验证、安全输入、安全输出和 nonce 的示例。

安全开发插件和主题

到目前为止,你已经熟悉了为 WordPress 开发插件和主题的基础知识。现在也是考虑编写代码时安全影响的好时机。

在本课中,你将学习如何以安全为出发点进行开发。

你将了解确保代码安全的好处、保护代码安全的步骤,以及在哪里可以找到更多关于以安全为出发点进行开发的信息。

免责声明

本课旨在作为开发插件和主题时培养安全意识的入门介绍。本课中使用的代码非常简化。你不应在插件或主题中使用本教程中的任何代码。

请务必阅读 WordPress 开发者手册中关于安全的完整文档,网址为 https://developer.wordpress.org/apis/security/,以确保你遵循正确的方法和流程。

什么是以安全为出发点进行开发?

以安全为出发点进行开发,是指确保你的代码不仅能够正常工作,而且不会引入任何安全漏洞的过程。

如果你的插件或主题代码存在安全漏洞,可能会使任何安装了你的产品的 WordPress 网站面临潜在攻击,并可能导致该网站被入侵。

在编写代码时,培养安全思维并思考你的代码可能被恶意利用的方式非常重要。

  • 不要信任任何数据,无论是用户输入、第三方 API 数据,甚至是数据库中的数据。始终检查以确保数据有效且使用安全。
  • WordPress 提供了许多 API,可以帮助你完成常见任务,例如清理用户输入、验证数据和转义输出。依赖这些 API 来帮助验证和清理数据,而不是编写自己的函数。
  • 及时了解常见漏洞,并保持代码更新以防止它们。

安全在开发过程中处于什么位置?

安全应该在开发过程的每个阶段都加以考虑。

通常,发现的大多数安全漏洞都发生在 PHP 层,该层在服务器上执行。

因此,本课将重点介绍 PHP 代码中最常见的预防措施。

清理输入

开发时要采取的首要步骤之一是确保任何用户输入都经过清理。这意味着任何来自用户的数据,例如表单提交或 URL 参数,都要经过检查以确保使用安全。

在此示例代码中,名称和电子邮件字段是从插件生成的表单提交的,然后保存在名为 form_submissions 的自定义数据库表中。

$name = $_POST['name'];
$email = $_POST['email'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

如你所见,数据直接保存到数据库,没有任何清理。

这意味着如果用户提交的名称为 John'; DROP TABLE form_submissions;--,SQL INSERT 查询将运行,随后是 DROP 查询,该表将从数据库中删除!

WordPress 提供了一个清理 API,可用于清理传入的数据。你可以使用 sanitize_text_fieldsanitize_email 函数在名称和电子邮件字段用于查询之前对其进行清理。

$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );

global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

请注意,代码遵循了清理数据的一个关键原则:尽可能早地进行清理。

要了解更多关于 WordPress 开发者可用的清理函数,请查看 WordPress 开发者文档中的“清理数据”页面。

验证数据 (4:06)

验证数据是根据预定义模式(或多个模式)对其进行测试的过程,结果明确:有效或无效。

不可信数据可能来自许多来源:用户、第三方 API 数据,甚至你的数据库数据也可能被视为不可信,尤其是当其他内容修改过它时。即使是网站管理员也可能犯错,输入不正确或不安全的数据,因此始终检查数据非常重要。

在此示例中,删除函数要求将数字 ID 发布到 admin-ajax 回调:

add_action( 'wp_ajax_delete_form_submission', 'wp_learn_delete_form_submission' );
function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

这里 ID 直接用于 SQL 查询,没有任何验证。同样,这意味着如果用户提交的 ID 为 1; DROP TABLE form_submissions;,相同的 SQL DROP 查询将在删除后运行,表将被删除。

为了防止这种情况,你可以使用 PHP 的类型转换功能,确保 $id 的值始终是整数。可以通过在变量名前添加 (int) 来实现,如下所示:

$id = (int) $_POST['id'];

请注意,这仅当通过 $_POST 数组传递的字符串的第一个字符可以转换为整数时才有效,否则 $id 的值将为 0。在这种情况下,最好更新代码以处理这种情况。

该代码还遵循了验证数据的一个关键原则,即尽早进行验证。

if ($id === 0){
    // return early with an error
    return wp_send_json( array( 'result' => 'Invalid ID passed' ) );

}

要了解更多关于验证数据的不同方法,请查看 WordPress 开发者文档中的“验证数据”部分。

转义输出

安全的另一个方面是确保你输出到浏览器的任何信息都是安全的,包括任何文本、HTML 或 JavaScript 代码,以及来自数据库的数据。

即使你的代码不负责所显示数据的来源,它也有责任安全地显示它。

在这个例子中,代码从数据库中获取表单提交数据,然后循环遍历这些提交数据,并在 WordPress 仪表盘的管理界面中输出提交数据:

    $submissions = wp_learn_get_form_submissions();
    ?>
    <div class="wrap" id="wp_learn_admin">
        <h1>Admin</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <?php foreach ($submissions as $submission){ ?>
                <tr>
                    <td><?php echo $submission->name; ?></td>
                    <td><?php echo $submission->email; ?></td>
                    <td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
                </tr>
            <?php } ?>
        </table>
    </div>
    <?php

这里我们有三个需要转义的数据项:$submission->name$submission->email 字段,以及 $submission->id

<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

对于名称和电子邮件字段,我们可以使用 WordPress 内置的转义函数 esc_html()。ID 可以通过将其转换为整数来转义,就像你在数据验证时可能做的那样。

  <td><?php echo esc_html( $submission->name ); ?></td>
  <td><?php echo esc_html( $submission->email ); ?></td>
  <td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

请注意,这段代码遵循了转义数据的一个关键原则,即尽可能晚地转义数据。

要了解更多关于转义输出的不同方法,请查看 WordPress 开发者文档中的“转义数据”部分。

防止无效请求

每当发出请求时,检查该请求是否有效非常重要。这意味着要检查请求是否来自可信来源。

例如,你可能有一个短代码用于渲染表单,用户可以通过它提交信息。

渲染表单的函数可能看起来像这样。

add_shortcode( 'wp_learn_form_shortcode', 'wp_learn_form_shortcode' );
function wp_learn_form_shortcode() {
    ob_start();
    ?>
    <form method="post">
        <input type="hidden" name="wp_learn_form" value="submit">
        <div>
            <label for="email">Name</label>
            <input type="text" id="name" name="name" placeholder="Name">
        </div>
        <div>
            <label for="email">Email address</label>
            <input type="text" id="email" name="email" placeholder="Email address">
        </div>
        <div>
            <input type="submit" id="submit" name="submit" value="Submit">
        </div>
    </form>
    <?php
    $form = ob_get_clean();
    return $form;
}
?>

当用户提交表单时,数据会被发送到服务器。然后数据被处理、清理并存储在数据库中。

add_action( 'wp', 'wp_learn_maybe_process_form' );
function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }

    $name = sanitize_text_field( $_POST['name'] );
    $email = sanitize_email( $_POST['email'] );

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
    $result = $wpdb->query($sql);
    if ( 0 < $result ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}

由于表单可能出现在任何使用该短代码的页面上,恶意用户可能会尝试向表单发送 POST 请求,要么寻找插件中的漏洞,要么发送多个请求。

为了防止这种情况,你可以检查请求是否来自可信来源。为此,你可以实现一种称为 nonce(一次性数字)的机制。

首先,在表单本身中,你可以通过使用 wp_nonce_field 函数添加一个 nonce 字段,并向该函数传递一个 nonce 操作和 nonce 名称:

wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );

当表单在前端渲染时,会向表单中添加一个隐藏字段,使用 nonce 名称作为隐藏字段的 id 和 name 属性,并将生成的 nonce 作为字段值。当表单提交时,这些数据会被发送。

然后,在处理表单数据的函数中,你可以通过使用 wp_verify_nonce 函数来验证 nonce 是否有效,并将 POST 提交的 nonce 字段和 nonce 操作传递给该函数。如果验证结果为 false,你可以提前退出,防止任何进一步的代码执行。

    if ( ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) {
        wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
        die();
    }

任何时候你的代码发出网络请求,无论是通过重定向到新 URL、向表单 POST 数据,还是发出 AJAX 请求,你都应该检查请求是否有效。

要了解更多关于如何在插件中使用 nonce,请查看 WordPress 开发者文档中的“Nonce”部分。

防止未认证用户

根据代码的功能,最好将某些功能限制为仅具有特定权限级别的用户使用。例如,你可能有一个从数据库中删除数据的函数。

function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = (int) $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

虽然你可能会尽力防止任何没有权限的人运行此函数,但针对这种情况添加检查仍然是一个好主意。

WordPress 包含一个强大的用户角色和权限系统,允许你使用默认的用户角色和权限,或创建自定义角色和权限。

在这种情况下,可以简单地只允许具有管理站点选项权限的用户执行操作,这是管理员角色中包含的标准权限。

function wp_learn_delete_form_submission() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return wp_send_json( array( 'result' => 'Authentication error' ) );
    }
    // rest of function code
}

WordPress 开发者文档中有一个关于用户角色和权限的详细部分,其中包含默认权限列表以及如何创建自定义权限。

进一步阅读

为了准备好以安全为出发点进行开发,请务必阅读 WordPress 开发者文档中关于安全性的条目,其中包含本课程中的所有示例,以及关于安全最佳实践、常见漏洞和更多示例代码的额外信息。

扩展WordPress REST API

WordPress REST API 提供了一种统一的接口,用于从 WordPress 站点获取、添加、更新和删除数据。

虽然 REST API 中可用的数据类型模式相当广泛,但有时你可能需要存储不属于核心模式的其他数据。

在本课中,你将学习两种在 REST API 请求中添加字段的方法:一种是在 REST API 路由中启用自定义字段,另一种是将自定义字段作为顶级字段提供。

你还将了解这两种方法的优缺点。

如果你跳过了本模块中的前几课,请从仓库自述文件中的链接下载 Bookstore 插件 1.0.2 版本,并在本地 WordPress 安装中安装并激活该插件。

此外,如果你还没有这样做,请下载并安装适用于你操作系统的 Postman 应用。

关于修改响应的重要说明

在开始之前,务必注意修改 WP REST API 响应可能会产生意外后果。从核心 REST API 端点响应中更改或删除数据可能会破坏插件或 WordPress 核心行为,应尽可能避免。

如果你需要从 REST API 请求中检索数据子集,推荐的方法是使用 _fields 全局参数来限制响应中返回的字段。

例如,如果你只需要应用程序中的 id 和 title 字段,可以使用 fields 参数将返回的字段限制为仅这两个。

向 REST API 响应添加字段风险较小,因此本课仅涵盖添加字段。

使用自定义字段

如果你观看了“WordPress 插件简介”模块中的课程,你应该已经了解了自定义字段(也称为元数据)。

这些字段通常用于自定义文章类型,以存储特定于该文章类型的额外数据。在底层,这些自定义字段存储在 postmeta 表中,作为一组键/值对,通过文章 ID 附加到文章上。

除了文章,WordPress 还支持其他数据类型(如评论和用户)的元数据。你可以在元数据 API 文档中了解更多信息。

WP REST API 允许你在创建或更新数据时创建或更新自定义字段。

这可以通过将键/值对对象传递给所操作文章类型的 meta 属性来实现。

但是,要使用自定义字段,你必须先注册它。这可以使用 register_meta 函数完成。

注册自定义字段

如果你想在文章上注册一个名为 location 的自定义字段,你可以像这样使用 register_meta 函数:

register_meta( 'post', 'location', array(
    'type'         => 'string',
    'description'  => 'A location for the post',
    'single'       => true,
    'show_in_rest' => true,
) );

虽然可以将其添加到插件的任何位置,但建议将其添加到类似 init 动作钩子的地方。

add_action( 'init', function() {
    register_meta( 'post', 'location', array(
        'type'         => 'string',
        'description'  => 'A location for the post',
        'single'       => true,
        'show_in_rest' => true,
    ) );
} );

务必确保 show_in_rest 属性设置为 true,否则自定义字段将无法在 REST API 中使用。

这将使自定义字段能够添加到文章的 REST API 模式中,并且还允许你使用 REST API 将数据发布到自定义字段。这通过在请求体的 meta 对象 中传递自定义字段作为键值对来处理。

{
    "title": "My Post",
    "content": "This is my post content.",
    "meta": {
        "location": "New York"
    }
}

你可以通过在 Postman 中创建一个新的 POST 请求并将其发送到文章路由来测试这一点。

为特定 WP REST API 路由启用自定义字段

WordPress 4.9.8 之前,使用 register_metashow_in_rest 设置为 true 的自定义字段会注册到给定类型的所有对象上。例如,如果你向文章类型添加了一个自定义字段,然后创建了一个自定义文章类型,该自定义字段将自动在自定义文章类型中可用。

WordPress 4.9.8 开始,可以使用带有 object_subtype 参数的 register_meta,该参数允许将元键的使用范围限制到特定的文章类型。

例如,假设你想仅在 book 自定义文章类型上注册一个 ISBN 自定义字段。你可以将其添加到注册自定义文章类型的代码中,如下所示:

register_post_type( 'book', array(
    'labels'      => array(
        'name'          => __( 'Books', 'bookstore' ),
        'singular_name' => __( 'Book', 'bookstore' ),
    ),
    'public'      => true,
    'has_archive' => true,
    'show_in_rest' => true,
    'supports'    => array( 'title', 'editor', 'custom-fields' ),
) );

register_meta( 'post', 'isbn', array(
    'type'         => 'string',
    'description'  => 'The ISBN of the book',
    'single'       => true,
    'show_in_rest' => true,
    'object_subtype' => 'book',
) );

现在,isbn 自定义字段将仅对 book 自定义文章类型可用。

你可以通过 REST API 添加一本书来测试这一点。

在 Postman 中,创建或更新对书籍路由的 POST 请求,并在 meta 对象中包含 ISBN 字段:

POST /wp-json/wp/v2/books

然后在请求体中发布以下内容:

{
    "title": "My Book",
    "content": "This is my book content.",
    "meta": {
        "isbn": "978-3-16-148410-0"
    }
}

如果你随后在 WordPress 管理后台编辑这本书,并且启用了自定义字段面板,你将看到 isbn 字段的值。它也是 book 文章类型唯一可用的自定义字段。

将自定义字段添加为 API 响应的顶级字段

另一种将自定义字段添加到 WP REST API 的方法是将它们添加为 API 响应的顶级字段。

在之前的示例中,isbn 被注册为元字段,因此它出现在 REST API 响应的元对象中。但如果你希望它作为顶级字段,与标题、内容和摘要并列,该怎么办呢?

这可以通过使用 register_rest_field 函数来实现。让我们看看如何实现这一点。

将自定义字段添加为顶级字段

首先,你需要在 rest_api_init 动作钩子中注册你的 REST 字段。这是为了确保该字段仅在 REST API 上注册。

add_action( 'rest_api_init', 'bookstore_add_rest_fields' );
function bookstore_add_rest_fields() { 
    // register some REST API functionality
}

然后,你使用 register_rest_field 函数来注册该字段。第一个参数是字段应注册的对象类型。对于单个对象,可以是字符串;对于多个对象,可以是数组。在本例中,只需在 book 自定义文章类型上注册该字段。第二个参数是字段的名称。在本例中,只需将其设置为与自定义字段相同的名称,即 isbn

    register_rest_field(
        'book',
        'isbn',
    );

第三个参数是一个参数数组,用于决定字段的功能。你至少需要向该数组传递以下三个参数。

  1. get_callback – 返回字段值的函数
  2. update_callback – 更新字段值的函数
  3. schema – 包含字段模式的数组
    register_rest_field(
        'book',
        'isbn',
        array(
            'get_callback'    => null,
            'update_callback' => null,
            'schema'          => null,
        )
    );

现在,你可以将 REST 字段的模式参数保留为 null,但你需要指定 get_callbackupdate_callback 函数。这些函数将在 API 请求发出时触发,无论是用于获取数据,还是用于创建或更新数据。

    register_rest_field(
        'book',
        'isbn',
        array(
            'get_callback'    => 'bookstore_rest_get_isbn',
            'update_callback' => 'bookstore_rest_update_isbn',
            'schema'          => null,
        )
    );

默认情况下,文章类型的准备数据数组会作为第一个参数传递给 get_callback 函数。这个函数的实现可以简单到直接返回自定义字段的值。

function bookstore_rest_get_isbn( $book ){
    return  get_post_meta( $book['id'], 'isbn', true );
}

从 REST API 创建或更新请求中为字段发送的值会作为第一个参数传递给 update_callback 函数,而模型对象(即文章)则作为第二个参数。这个函数的实现可以简单到直接更新自定义字段的值。

function bookstore_rest_update_isbn( $value, $book ){
    return update_post_meta( $book->ID, 'isbn', $value );
}

如果你通过创建一本新书并为 isbn 字段传递一个值来测试这一点,你会看到数据被保存到数据库的 post_meta 表中,同时也会作为顶级字段显示在 REST API 响应中。

包含模式

模式参数是一个描述字段模式的数组。虽然这不是必须的,但建议包含模式。至少,它有助于未来的开发者理解字段的用途。它还可以用于在创建自动化 API 测试时验证发送的数据。

    register_rest_field(
        'book',
        'isbn',
        array(
            'get_callback'    => 'wp_learn_rest_get_isbn',
            'update_callback' => 'wp_learn_rest_update_isbn',
            'schema'          => array(
                'description' => __( 'The ISBN of the book' ),
                'type'        => 'string',
            ),
        )
    );

你可以在 WP REST API 手册中阅读更多关于如何定义 REST API 资源和字段模式的内容。

是否使用 register_rest_field

在决定是仅使用 register_meta,还是添加 register_rest_field 的使用时,你应该考虑每种方法的优缺点。

仅使用 register_meta 方法的主要优点是,只要你记得在应用程序代码中使用元对象来获取和保存数据,就不需要添加任何额外的代码来启用自定义字段的存储或检索。你只需启用该字段在 REST API 中显示,就可以直接使用它。这也是性能更优的选择,因为它不会增加任何需要执行的额外代码。

另一方面,使用 register_rest_field 方法的优点是,你可以在数据返回或保存之前对其进行额外的处理。例如,你可以在数据保存到数据库之前对其进行一些验证。你还可以向 get_callback 和 update_callback 函数添加钩子,以便对数据进行额外处理,或允许其他开发者扩展你的自定义字段。缺点是它会为 API 请求增加一些开销,因为需要执行更多的代码。

最终,你选择哪种方法应根据具体情况来决定。

进一步阅读

有关修改 REST API 响应的更多信息,请查看 WP REST API 手册中的“修改响应”部分。