概述

文件系统 API 于WordPress 2.6版本中引入,最初是为 WordPress 自身的自动更新功能而创建的。

文件系统 API 抽象出了读取和写入本地文件所需的功能,使其能够在各种主机类型上安全地执行。

它通过WP_Filesystem_Base类以及多个子类来实现这一点,这些子类根据各自主机的支持情况,实现了连接到本地文件系统的不同方式。

任何需要写入本地文件的主题或插件都应使用WP_Filesystem类族来完成操作。

目的

不同的托管系统在 Web 服务器配置方面有不同的限制。

特别是,许多托管系统让 Web 服务器以与 WordPress 文件所有者不同的用户身份运行。在这种情况下,从 Web 服务器用户进程写入的文件将由 Web 服务器的用户账户拥有,而不是实际用户的账户。在共享托管环境中,这可能会导致安全问题,因为多个用户为不同站点共享同一台 Web 服务器。

WP_Filesystem能够检测到写入文件的用户不匹配的情况,并切换到使用 FTP 或其他类似方法。根据可用的 PHP 库,WP_Filesystem支持三种不同的 FTP 使用方法(通过扩展、套接字或 SSH),并会自动选择正确的方法。

在此情况下,实现此代码的插件或主题需要向用户请求 FTP 凭据。已添加相关函数以简化此操作,并使凭据输入表单的外观和感觉标准化。

文件系统 API 类参考

获取凭据

使用 WP_Filesystem 的第一步是向用户请求凭据。通常,这是在保存表单输入结果时完成,或者当你确定需要写入文件时进行。

可以通过以下代码将凭据表单显示到管理页面上:

$url = wp_nonce_url( 'themes.php?page=example', 'example-theme-options' );
if ( false === ( $creds = request_filesystem_credentials( $url, '', false, false, null ) ) ) {
	return; // stop processing here
}

request_filesystem_credentials()调用接受五个参数。

  • 表单应提交到的 URL(上面的示例中使用了一个指向主题页面的 nonce URL)
  • 方法覆盖(通常应保留为空字符串:“”)
  • 错误标志(通常为 false,除非检测到错误,见下文)
  • 上下文目录(false,或你想要测试访问权限的特定目录路径)
  • 表单字段(来自你之前表单的表单字段名称数组,你希望将这些字段“透传”到生成的凭据表单中,如果没有则为 null)

request_filesystem_credentials调用会首先测试是否无需凭据即可直接写入本地文件系统。如果是这种情况,它将返回 true 且不执行任何操作。你的代码随后可以使用WP_Filesystem类。

request_filesystem_credentials调用还会考虑硬编码的信息,例如通过 defines 插入到wp-config.php文件中的主机名、用户名或密码。如果这些信息在该文件中已预定义,则该调用将返回这些信息而不是显示表单,从而绕过用户表单。

如果确实需要从用户处获取凭据,它将输出 FTP 信息表单并返回 false。在这种情况下,你应该停止进一步处理,以便让用户输入凭据。你指定的任何表单字段名称都将作为隐藏输入包含在生成的表单中,并在用户重新提交表单时返回,这次带有 FTP 凭据。

注意:不要使用hostname、username、password、public_key或private_key这些保留名称作为你自己的输入。这些名称由凭据表单本身使用。或者,如果你使用了它们,request_filesystem_credentials函数将假设它们是传入的 FTP 凭据。

当提交凭据表单时,它将在传入的 POST 数据中查找这些字段,如果找到,它将返回一个适合传递给 WP_Filesystem 的数组,这是下一步操作。

初始化 WP_Filesystem_Base

在使用 WP_Filesystem 之前,必须使用正确的凭据对其进行初始化。可以如下操作:

if ( ! WP_Filesystem( $creds ) ) {
	request_filesystem_credentials( $url, '', true, false, null );
	return;
}

首先调用WP_Filesystem函数,并传递之前的凭据。它将尝试验证凭据。如果凭据有效,则返回 true;否则返回 false。

在凭据错误的情况下,上述代码再次调用request_filesystem_credentials(),但此次将错误标志设置为 true。这会强制函数再次显示表单,同时向用户显示一条错误消息,说明其信息不正确。用户可以重新输入信息并再次尝试。

使用 WP_Filesystem_Base 类

一旦类被初始化,全局变量$wp_filesystem就会被定义并可供你使用。WP_Filesystem_Base类定义了多种可用于读写本地文件的方法。例如,要写入文件,你可以这样做:

global $wp_filesystem;
$wp_filesystem->put_contents(
  '/tmp/example.txt',
  'Example contents of a file',
  FS_CHMOD_FILE // predefined mode settings for WP files
);

其他可用方法包括用于读取文件的get_contents()和get_contents_array();返回这些目录文件系统路径的wp_content_dir()、wp_plugins_dir()和wp_themes_dir();用于创建和删除目录的mkdir()和;以及其他几个有用的文件系统相关函数。

技巧与提示

何时可以调用request_filesystem_credentials()?
对于使用 WP Filesystem API 的开发人员来说,其中一个初始挑战是你不能在任何地方初始化它。request_filesystem_credentials()函数仅在wp_loaded动作钩子之后才可用,并且仅包含在管理区域中。你可以利用的最早钩子之一是admin_init。

WP Filesystem API 方法论
直接调用request_filesystem_credentials()的另一个问题是,你无法确定是直接访问文件系统还是需要提示用户输入凭据。从用户体验的角度来看,如果你想在一个插件激活时修改文件,这就变得有问题了。试想一下,用户通过他们的管理区域安装你的插件,输入 FTP 详细信息,完成安装并激活你的插件。但就在他们这么做之后,系统立即提示他们再次输入 FTP 详细信息,让他们一头雾水。

更好的解决方案是添加一条通知(例如使用admin_notice),向用户解释你的插件需要写入文件系统才能完成安装。除了这条通知外,你还可以添加一个按钮或链接来触发你对request_filesystem_credentials()的函数调用。

但让我们进一步扩展这个场景,假设该插件每次更新时都需要访问文件系统。如果你定期发布更新和错误修复,用户每次升级时点击你的可操作按钮很快就会变得令人难以忍受。如果能先确定我们是否有直接写入权限,然后再调用request_filesystem_credentials()并静默地完成安装就好了。这正是get_filesystem_method()函数发挥作用的地方。

$access_type = get_filesystem_method();
if ( $access_type === 'direct' )
{
	/* 你可以安全地运行 request_filesystem_credentials() 而不会有任何问题,也无需担心传入 URL */
	$creds = request_filesystem_credentials( site_url() . '/wp-admin/', '', false, false, array() );
	/* 初始化 API */
	if ( ! WP_Filesystem( $creds ) ) {
		/* 出现任何问题则退出 */
		return false;
	}	
	global $wp_filesystem;
	/* 在下面进行文件操作 */
}	
else
{
	/* 没有直接写入权限。通过我们的通知提示用户 */
	add_action( 'admin_notices', 'you_admin_notice_function' ); 	
}

这种方法对所有相关方都很有效。没有直接写入权限的用户会被提示修改文件系统,而对于能够直接写入文件系统的站点,插件则悄无声息地运行(这是好事)。

处理路径
称职的 WordPress 开发者应该熟悉设置常量或变量以访问其插件路径的方法。通常如下所示:

define( 'MY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 

在使用 Filesystem API 时,你需要考虑的是文件路径并不总是相同的。当使用 direct 方法时,你可以安全地使用 MY_PLUGIN_DIR 常量,但如果尝试在 FTP 或 SSH 方法下这样做,就会遇到问题。这是因为 FTP 和 SSH 通常根植于绝对路径中的某个目录。现在,Filesystem API 提供了像 $wp_filesystem->wp_content_dir() 和 $wp_filesystem->wp_plugins_dir() 这样的方法来克服这个问题,但为插件定义两次路径并不实用。

/* 将 'direct' 绝对路径替换为 Filesystem API 路径 */
 $plugin_path = str_replace( ABSPATH, $wp_filesystem->abspath(), MY_PLUGIN_DIR );
/* 现在我们可以在所有 Filesystem API 方法调用中使用 $plugin_path */
if ( ! $wp_filesystem->is_dir( $plugin_path . '/config/' ) ) {
	/* 目录不存在,让我们创建它 */
	$wp_filesystem->mkdir( $plugin_path . '/config/' );
}

unzip_file($file, $to);

虽然此函数需要初始化 Filesystem API,但它不是 $wp_filesystem 对象的方法,这可能就是为什么它的参数彼此不一致的原因。第一个参数 $file 需要是文件的绝对'direct'路径,而 $toparameter 需要指向 Filesystem 的绝对路径。

define( 'MY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 
global $wp_filesystem; // 之前已初始化 Filesystem API
$plugin_path = str_replace( ABSPATH, $wp_filesystem->abspath(), MY_PLUGIN_DIR ); // 获取远程系统绝对路径
/* 使用该函数的可接受方式 */
$file = MY_PLUGIN_DIR . '/plugin-file.zip'; 
$to = $plugin_path;
$result = unzip_file( $file, $to ); 
if ( $result !== true ) {
	// 解压失败。处理错误
}
/* 不可接受 */
$file = MY_PLUGIN_DIR . '/plugin-file.zip';
$to = MY_PLUGIN_DIR; // $to 不能是文件夹的'direct'绝对路径,否则 FTP 和 SSH 方法将被弃用
unzip_file( $file, $to ); 
$file = $plugin_path . '/plugin-file.zip'; // 如果 $file 不是'direct'绝对路径,那么不使用 FTP 和 SSH 方法的用戶将被弃用
$to = $plugin_path;
unzip_file( $file, $to );

外部引用