探究 WordPress `WP_Filesystem_Direct` 类的源码:它是如何直接通过 PHP 内置函数操作文件系统的。

各位朋友,大家好!今天咱们聊聊WordPress里一个“简单粗暴”的家伙:WP_Filesystem_Direct。 别被它的名字唬住,其实它就是个“直肠子”,直接用PHP的内置函数跟服务器的文件系统“硬碰硬”。咱们一起扒一扒它的源码,看看它是怎么干活的。

开场白:为啥要有 WP_Filesystem

在深入 WP_Filesystem_Direct 之前,先简单说说 WP_Filesystem 的作用。想象一下,WordPress要安装插件、更新主题、修改配置文件,都需要操作服务器上的文件。但不是每个人都有权限直接操作服务器。 有些服务器可能限制了PHP的执行权限,或者使用了FTP、SSH等方式来管理文件。

WP_Filesystem 就是一个抽象层,它把各种文件操作方式封装起来,让WordPress可以统一地操作文件,而不用关心底层到底是用哪种方式。 就像你用遥控器控制电视,不用管电视内部是用什么电路工作的。

WP_Filesystem_Direct 就是 WP_Filesystem 的一个实现类,也是最简单的一种实现。它假设你的PHP脚本有足够的权限直接操作文件系统,所以它直接调用PHP的内置函数来完成文件操作。

WP_Filesystem_Direct 类:源码剖析

咱们直接看代码,边看边聊:

<?php

class WP_Filesystem_Direct {

    /**
     * Constructor.
     *
     * @since 2.5.0
     *
     * @param mixed $arg Ignored.
     */
    public function __construct( $arg = '' ) {}

    /**
     * Connect to the Filesystem.
     *
     * @since 2.5.0
     *
     * @return true Always returns true.
     */
    public function connect() {
        return true;
    }

    /**
     * Get the last error that occurred.
     *
     * @since 2.5.0
     *
     * @return false Always returns false.
     */
    public function errors() {
        return false;
    }

    /**
     * Determine if the connection succeeded.
     *
     * @since 2.5.0
     *
     * @return true Always returns true.
     */
    public function connected() {
        return true;
    }

    /**
     * Read a file.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return string|false The file contents on success, false on failure.
     */
    public function get_contents( $file ) {
        return @file_get_contents( $file );
    }

    /**
     * Write a string to a file.
     *
     * @since 2.5.0
     *
     * @param string $file     Path to the file.
     * @param string $contents The file contents.
     * @param int    $mode     Optional. Permissions for the new file. Default false.
     * @return bool True on success, false on failure.
     */
    public function put_contents( $file, $contents, $mode = false ) {
        if ( ! $mode ) {
            return @file_put_contents( $file, $contents );
        } else {
            return @file_put_contents( $file, $contents, $mode );
        }
    }

    /**
     * Read a file into an array.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return array|false The file contents as an array, false on failure.
     */
    public function get_contents_array( $file ) {
        return @file( $file );
    }

    /**
     * Get the current file permissions.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return string|false Permissions of the file, false on failure.
     */
    public function get_chmod( $file ) {
        return @substr( decoct( fileperms( $file ) ), -3 );
    }

    /**
     * Change the file permissions.
     *
     * @since 2.5.0
     *
     * @param string $file  Path to the file.
     * @param int    $chmod Permissions as octal number.
     * @return bool True on success, false on failure.
     */
    public function chmod( $file, $chmod = false ) {
        if ( ! $chmod ) {
            if ( $this->is_writable( $file ) ) {
                return true;
            } else {
                return false;
            }
        }
        return @chmod( $file, octdec( $chmod ) );
    }

    /**
     * Get the file owner.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return string|false The owner of the file, false on failure.
     */
    public function owner( $file ) {
        if ( ! function_exists( 'posix_getpwuid' ) ) {
            return false;
        }
        $owner_id = @fileowner( $file );
        if ( ! $owner_id ) {
            return false;
        }
        $owner_array = @posix_getpwuid( $owner_id );
        if ( ! $owner_array ) {
            return false;
        }
        return $owner_array['name'];
    }

    /**
     * Get the file group.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return string|false The group of the file, false on failure.
     */
    public function group( $file ) {
        if ( ! function_exists( 'posix_getgrgid' ) ) {
            return false;
        }
        $group_id = @filegroup( $file );
        if ( ! $group_id ) {
            return false;
        }
        $group_array = @posix_getgrgid( $group_id );
        if ( ! $group_array ) {
            return false;
        }
        return $group_array['name'];
    }

    /**
     * Determine if a file is writable.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return bool True if the file is writable, false otherwise.
     */
    public function is_writable( $file ) {
        return @is_writable( $file );
    }

    /**
     * Determine if a file exists.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return bool True if the file exists, false otherwise.
     */
    public function exists( $file ) {
        return @file_exists( $file );
    }

    /**
     * Determine if a file is a file.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return bool True if the file is a file, false otherwise.
     */
    public function is_file( $file ) {
        return @is_file( $file );
    }

    /**
     * Determine if a directory is a directory.
     *
     * @since 2.5.0
     *
     * @param string $path Path to the directory.
     * @return bool True if the path is a directory, false otherwise.
     */
    public function is_dir( $path ) {
        return @is_dir( $path );
    }

    /**
     * Determine if a file is a symlink.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return bool True if the file is a symlink, false otherwise.
     */
    public function is_link( $file ) {
        return @is_link( $file );
    }

    /**
     * Is a file or directory empty?
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return bool True if the file/folder is empty, false otherwise.
     */
    public function is_empty( $file ) {
        if ( ! $this->exists( $file ) ) {
            return true;
        }

        if ( $this->is_file( $file ) ) {
            return ( 0 == @filesize( $file ) );
        } elseif ( is_readable( $file ) ) {
            $filelist = @scandir( $file );
            if ( ! $filelist || 2 == count( $filelist ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Create a directory.
     *
     * @since 2.5.0
     *
     * @param string $path      Path to create.
     * @param int    $chmod     Optional. Permissions for the new directory. Default false.
     * @param bool   $recursive Optional. Whether to create parent directories. Default false.
     * @return bool True on success, false on failure.
     */
    public function mkdir( $path, $chmod = false, $recursive = false ) {
        if ( ! $chmod ) {
            $chmod = FS_CHMOD_DIR;
        }

        if ( $recursive ) {
            return @mkdir( $path, octdec( $chmod ), true );
        } else {
            return @mkdir( $path, octdec( $chmod ) );
        }
    }

    /**
     * Delete a directory.
     *
     * @since 2.5.0
     *
     * @param string $path      Path to delete.
     * @param bool   $recursive Optional. Whether to delete recursively. Default false.
     * @return bool True on success, false on failure.
     */
    public function rmdir( $path, $recursive = false ) {
        if ( $recursive ) {
            return $this->delete( $path, true );
        } else {
            return @rmdir( $path );
        }
    }

    /**
     * Upload a file.
     *
     * @since 2.5.0
     *
     * @param string $file     Path to the source file.
     * @param string $destination Path to the destination file.
     * @return bool True on success, false on failure.
     */
    public function upload( $file, $destination ) {
        return @copy( $file, $destination );
    }

    /**
     * Copy a file.
     *
     * @since 2.5.0
     *
     * @param string $source      Path to the source file.
     * @param string $destination Path to the destination file.
     * @param bool   $overwrite   Optional. Whether to overwrite the destination file. Default false.
     * @param int    $mode        Optional. Permissions for the new file. Default false.
     * @return bool True on success, false on failure.
     */
    public function copy( $source, $destination, $overwrite = false, $mode = false ) {
        if ( ! $overwrite && $this->exists( $destination ) ) {
            return false;
        }

        if ( ! $mode ) {
            return @copy( $source, $destination );
        } else {
            //PHP < 5.3 doesn't support copy with context. Use workaround
            $content = $this->get_contents( $source );
            if ( false === $content ) {
                return false;
            }
            return $this->put_contents( $destination, $content, $mode );

        }
    }

    /**
     * Move a file.
     *
     * @since 2.5.0
     *
     * @param string $source      Path to the source file.
     * @param string $destination Path to the destination file.
     * @param bool   $overwrite   Optional. Whether to overwrite the destination file. Default false.
     * @return bool True on success, false on failure.
     */
    public function move( $source, $destination, $overwrite = false ) {
        if ( ! $overwrite && $this->exists( $destination ) ) {
            return false;
        }

        return @rename( $source, $destination );
    }

    /**
     * Delete a file or directory.
     *
     * @since 2.5.0
     *
     * @param string $path      Path to delete.
     * @param bool   $recursive Optional. Whether to delete recursively. Default false.
     * @param string $type      Optional. What type of resource to delete. Default false.
     *                           'f' for file, 'd' for directory.
     * @return bool True on success, false on failure.
     */
    public function delete( $path, $recursive = false, $type = false ) {
        if ( empty( $path ) ) {
            return false;
        }

        if ( 'f' == $type || $this->is_file( $path ) ) {
            return @unlink( $path );
        }

        if ( ! $recursive && $this->is_dir( $path ) ) {
            return @rmdir( $path );
        }

        if ( ! $this->is_dir( $path ) ) {
            return false;
        }

        // Use glob() to support wildcards
        $files = glob( trailingslashit( $path ) . '*' );
        if ( ! empty( $files ) ) {
            foreach ( $files as $file ) {
                if ( is_dir( $file ) ) {
                    $this->delete( $file, true );
                } else {
                    @unlink( $file );
                }
            }
        }

        return @rmdir( $path );
    }

    /**
     * Get the file modification time.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return int|false The file modification time, false on failure.
     */
    public function mtime( $file ) {
        return @filemtime( $file );
    }

    /**
     * Get the file size.
     *
     * @since 2.5.0
     *
     * @param string $file Path to the file.
     * @return int|false The file size, false on failure.
     */
    public function size( $file ) {
        return @filesize( $file );
    }

    /**
     * Get the file system root path.
     *
     * @since 2.5.0
     *
     * @param string $file Optional. Path to a file.
     * @return string The file system root path.
     */
    public function dirlist( $path, $include_hidden = false, $recursive = false ) {
        if ( ! $this->is_dir( $path ) ) {
            return false;
        }

        $inner = array();
        $dir   = @opendir( $path );

        if ( $dir ) {
            while ( false !== ( $entry = readdir( $dir ) ) ) {
                if ( '.' == $entry || '..' == $entry ) {
                    continue;
                }

                if ( ! $include_hidden && '.' == $entry[0] ) {
                    continue;
                }

                $fullpath = trailingslashit( $path ) . $entry;

                if ( $this->is_dir( $fullpath ) ) {
                    $type = 'd';

                    if ( $recursive ) {
                        $inner[ $entry ] = $this->dirlist( $fullpath, $include_hidden, $recursive );
                    } else {
                        $inner[ $entry ] = array( 'name' => $entry, 'type' => 'd' );
                    }
                } else {
                    $type = 'f';
                    $inner[ $entry ] = array( 'name' => $entry, 'type' => 'f' );
                }

                if ( 'd' == $type ) {
                    if ( ! isset( $inner[ $entry ]['size'] ) ) {
                        $inner[ $entry ]['size'] = $this->size( $fullpath );
                    }
                } else {
                    $inner[ $entry ]['size'] = $this->size( $fullpath );
                }

                $inner[ $entry ]['perms'] = $this->get_chmod( $fullpath );
                $inner[ $entry ]['owner'] = $this->owner( $fullpath );
                $inner[ $entry ]['group'] = $this->group( $fullpath );
                $inner[ $entry ]['lastmodunix'] = $this->mtime( $fullpath );
                $inner[ $entry ]['lastmod'] = date( 'F d, Y g:i:s A', $inner[ $entry ]['lastmodunix'] );
            }
            @closedir( $dir );
        }

        return $inner;
    }
}

重点方法解读

咱们挑几个有代表性的方法,仔细看看:

  • get_contents( $file )put_contents( $file, $contents, $mode = false )

    这两个方法分别负责读取和写入文件内容。它们直接使用了PHP的 file_get_contents()file_put_contents() 函数。

    public function get_contents( $file ) {
        return @file_get_contents( $file );
    }
    
    public function put_contents( $file, $contents, $mode = false ) {
        if ( ! $mode ) {
            return @file_put_contents( $file, $contents );
        } else {
            return @file_put_contents( $file, $contents, $mode );
        }
    }

    注意 @ 符号: 这个符号的作用是抑制错误显示。如果 file_get_contents()file_put_contents() 出错(比如文件不存在、权限不足),PHP会抛出一个错误。@ 符号可以阻止这个错误显示出来,让代码更健壮一些。 虽然抑制错误有时候不是好习惯,但在这里,由于 WP_Filesystem 是一个抽象层,它需要自己处理错误,而不是让PHP直接抛出错误。

  • mkdir( $path, $chmod = false, $recursive = false )

    这个方法用于创建目录。它使用了PHP的 mkdir() 函数。

    public function mkdir( $path, $chmod = false, $recursive = false ) {
        if ( ! $chmod ) {
            $chmod = FS_CHMOD_DIR; //FS_CHMOD_DIR 通常定义在 wp-config.php 或者其它配置文件中,表示默认的目录权限。
        }
    
        if ( $recursive ) {
            return @mkdir( $path, octdec( $chmod ), true );
        } else {
            return @mkdir( $path, octdec( $chmod ) );
        }
    }
    • $chmod: 指定了新目录的权限。如果未指定,则使用 FS_CHMOD_DIR 常量作为默认权限。
    • $recursive: 指定是否递归创建目录。如果为 true,则会创建所有不存在的父目录。
    • octdec( $chmod ): 将八进制权限转换为十进制。mkdir() 函数需要十进制的权限值。

    例子:

    $fs = new WP_Filesystem_Direct();
    $path = '/path/to/new/directory';
    $chmod = '0755'; //八进制权限
    $recursive = true;
    
    $result = $fs->mkdir( $path, $chmod, $recursive );
    
    if ( $result ) {
        echo '目录创建成功!';
    } else {
        echo '目录创建失败!';
    }
  • delete( $path, $recursive = false, $type = false )

    这个方法用于删除文件或目录。它使用了PHP的 unlink()rmdir() 函数。

    public function delete( $path, $recursive = false, $type = false ) {
        if ( empty( $path ) ) {
            return false;
        }
    
        if ( 'f' == $type || $this->is_file( $path ) ) {
            return @unlink( $path );
        }
    
        if ( ! $recursive && $this->is_dir( $path ) ) {
            return @rmdir( $path );
        }
    
        if ( ! $this->is_dir( $path ) ) {
            return false;
        }
    
        // Use glob() to support wildcards
        $files = glob( trailingslashit( $path ) . '*' );
        if ( ! empty( $files ) ) {
            foreach ( $files as $file ) {
                if ( is_dir( $file ) ) {
                    $this->delete( $file, true );
                } else {
                    @unlink( $file );
                }
            }
        }
    
        return @rmdir( $path );
    }
    • $recursive: 指定是否递归删除目录。如果为 true,则会删除目录中的所有文件和子目录。
    • $type: 指定要删除的资源类型。'f' 表示文件,'d' 表示目录。如果未指定,则根据路径判断资源类型。

    例子:

    $fs = new WP_Filesystem_Direct();
    $path = '/path/to/file.txt';
    $result = $fs->delete( $path );
    
    if ( $result ) {
        echo '文件删除成功!';
    } else {
        echo '文件删除失败!';
    }
    
    $path = '/path/to/directory';
    $recursive = true;
    $result = $fs->delete( $path, $recursive );
    
    if ( $result ) {
        echo '目录删除成功!';
    } else {
        echo '目录删除失败!';
    }
  • dirlist( $path, $include_hidden = false, $recursive = false )

    这个方法用于列出目录中的文件和子目录。 它使用了PHP的 opendir()readdir()closedir() 以及 scandir() 函数, 并且递归调用自身来处理子目录。

    public function dirlist( $path, $include_hidden = false, $recursive = false ) {
        if ( ! $this->is_dir( $path ) ) {
            return false;
        }
    
        $inner = array();
        $dir   = @opendir( $path );
    
        if ( $dir ) {
            while ( false !== ( $entry = readdir( $dir ) ) ) {
                if ( '.' == $entry || '..' == $entry ) {
                    continue;
                }
    
                if ( ! $include_hidden && '.' == $entry[0] ) {
                    continue;
                }
    
                $fullpath = trailingslashit( $path ) . $entry;
    
                if ( $this->is_dir( $fullpath ) ) {
                    $type = 'd';
    
                    if ( $recursive ) {
                        $inner[ $entry ] = $this->dirlist( $fullpath, $include_hidden, $recursive );
                    } else {
                        $inner[ $entry ] = array( 'name' => $entry, 'type' => 'd' );
                    }
                } else {
                    $type = 'f';
                    $inner[ $entry ] = array( 'name' => $entry, 'type' => 'f' );
                }
    
                if ( 'd' == $type ) {
                    if ( ! isset( $inner[ $entry ]['size'] ) ) {
                        $inner[ $entry ]['size'] = $this->size( $fullpath );
                    }
                } else {
                    $inner[ $entry ]['size'] = $this->size( $fullpath );
                }
    
                $inner[ $entry ]['perms'] = $this->get_chmod( $fullpath );
                $inner[ $entry ]['owner'] = $this->owner( $fullpath );
                $inner[ $entry ]['group'] = $this->group( $fullpath );
                $inner[ $entry ]['lastmodunix'] = $this->mtime( $fullpath );
                $inner[ $entry ]['lastmod'] = date( 'F d, Y g:i:s A', $inner[ $entry ]['lastmodunix'] );
            }
            @closedir( $dir );
        }
    
        return $inner;
    }
    • $include_hidden: 指定是否包含隐藏文件。
    • $recursive: 指定是否递归列出子目录。

    返回结果:

    dirlist() 方法返回一个多维数组,包含了目录中的文件和子目录的信息。 数组的结构大致如下:

    array(
        'file1.txt' => array(
            'name' => 'file1.txt',
            'type' => 'f',
            'size' => 1024,
            'perms' => '755',
            'owner' => 'www-data',
            'group' => 'www-data',
            'lastmodunix' => 1678886400,
            'lastmod' => 'March 15, 2023 12:00:00 AM'
        ),
        'dir1' => array(
            'name' => 'dir1',
            'type' => 'd',
            'size' => 4096,
            'perms' => '755',
            'owner' => 'www-data',
            'group' => 'www-data',
            'lastmodunix' => 1678886400,
            'lastmod' => 'March 15, 2023 12:00:00 AM',
            'file2.txt' => array( // 如果 recursive 为 true,这里会包含子目录的内容
                'name' => 'file2.txt',
                'type' => 'f',
                'size' => 2048,
                'perms' => '755',
                'owner' => 'www-data',
                'group' => 'www-data',
                'lastmodunix' => 1678886400,
                'lastmod' => 'March 15, 2023 12:00:00 AM'
            )
        )
    )

    例子:

    $fs = new WP_Filesystem_Direct();
    $path = '/path/to/directory';
    $result = $fs->dirlist( $path, false, true );
    
    if ( $result ) {
        echo '<pre>';
        print_r( $result );
        echo '</pre>';
    } else {
        echo '无法列出目录!';
    }

WP_Filesystem_Direct 的优缺点

优点 缺点
速度快: 直接调用PHP内置函数,没有额外的开销。 安全性: 需要PHP有足够的权限操作文件系统,这可能会带来安全风险。 如果服务器配置不当,可能会导致恶意用户通过PHP脚本修改或删除重要文件。
简单易用: 代码简单,容易理解和维护。 适用性有限: 只能在PHP有足够权限操作文件系统的服务器上使用。在一些安全限制较多的服务器上,可能需要使用其他的 WP_Filesystem 实现类,比如 WP_Filesystem_FTP
无需额外配置: 不需要安装额外的扩展或配置。 错误处理: 依赖于PHP的错误处理机制,可能不够灵活。虽然使用了 @ 符号来抑制错误显示,但仍然需要额外的代码来处理错误。
直接操作: 可以直接操作文件系统,对于一些需要底层操作的场景很有用。 权限依赖: 它的成功运行完全依赖于PHP进程的用户权限。 如果PHP进程的用户没有足够的权限来访问或修改文件,WP_Filesystem_Direct 将无法正常工作。 这意味着它可能不适用于所有环境,特别是在共享主机或安全性要求较高的服务器上。
资源消耗少: 由于直接使用PHP内置函数,资源消耗相对较少。 缺乏抽象: 虽然 WP_Filesystem 提供了抽象层,但 WP_Filesystem_Direct 本身并没有对文件操作进行额外的抽象或封装。 这意味着代码与底层文件系统操作紧密耦合,如果需要更换文件操作方式(例如,使用不同的文件系统或协议),则需要修改大量代码。
内置支持: WP_Filesystem_Direct 是 WordPress 核心的一部分,无需额外安装或引入。 缺乏高级功能: WP_Filesystem_Direct 主要提供基本的文件操作功能,例如读取、写入、创建、删除等。 它缺乏一些高级功能,例如文件压缩、解压缩、加密、解密等。 如果需要这些高级功能,可能需要使用其他的 WP_Filesystem 实现类或第三方库。

使用场景

  • 本地开发环境: 在本地开发环境中,通常PHP有足够的权限操作文件系统,可以使用 WP_Filesystem_Direct
  • 一些权限宽松的服务器: 在一些权限宽松的服务器上,如果确定PHP有足够的权限,可以使用 WP_Filesystem_Direct
  • 对性能要求高的场景: 如果对文件操作的性能要求很高,可以使用 WP_Filesystem_Direct,因为它没有额外的开销。

总结

WP_Filesystem_DirectWP_Filesystem 的一个简单而直接的实现类。 它直接使用PHP的内置函数来操作文件系统,速度快,易于使用,但安全性较低,适用性有限。 在选择使用 WP_Filesystem_Direct 时,需要权衡其优缺点,并根据实际情况选择合适的 WP_Filesystem 实现类。

希望今天的讲解对你有所帮助! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注