put_contents( $path, $contents ); } return false !== file_put_contents( $path, $contents, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- the direct method is unavailable here. } /** * Moves a file, replacing the target; both ends have to sit inside $boundary. * * @param string|null $from_boundary Folder the source has to sit inside when it lives somewhere else than the target. */ public static function move( string $from, string $to, string $boundary, ?string $from_boundary = null ): bool { if ( is_link( $from ) || ! self::inside( dirname( $from ), $from_boundary ?? $boundary ) || ! self::inside( dirname( $to ), $boundary ) ) { return false; } $fs = self::direct( $boundary ); if ( null !== $fs ) { return (bool) $fs->move( $from, $to, true ); } return rename( $from, $to ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- the direct method is unavailable here. } /** * Removes a folder and everything under it, inside $boundary. * A link is removed itself and never followed, at the top or further down. */ public static function delete_tree( string $dir, string $boundary ): bool { if ( is_link( $dir ) ) { wp_delete_file( $dir ); return ! is_link( $dir ); } if ( ! is_dir( $dir ) ) { return true; } if ( ! self::inside( $dir, $boundary ) ) { return false; } $fs = self::direct( $boundary ); foreach ( (array) scandir( $dir ) as $name ) { if ( '.' === $name || '..' === $name ) { continue; } $path = $dir . '/' . $name; if ( is_dir( $path ) && ! is_link( $path ) ) { self::delete_tree( $path, $boundary ); } elseif ( null !== $fs && ! is_link( $path ) ) { $fs->delete( $path, false, 'f' ); } else { wp_delete_file( $path ); } } if ( null !== $fs ) { return (bool) $fs->rmdir( $dir ); } return rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- the direct method is unavailable here. } /** Whether a path resolves to $boundary or somewhere under it. */ private static function inside( string $path, string $boundary ): bool { $real_path = realpath( $path ); $real_boundary = realpath( $boundary ); if ( false === $real_path || false === $real_boundary ) { return false; } return $real_path === $real_boundary || str_starts_with( $real_path, rtrim( $real_boundary, '/\\' ) . DIRECTORY_SEPARATOR ); } /** * A local direct filesystem, or null when WordPress would not choose that method. * It is built here rather than through WP_Filesystem() so no unconnected FTP object is left in the global. */ private static function direct( string $context ): ?\WP_Filesystem_Base { $loader = ABSPATH . 'wp-admin/includes/file.php'; if ( ! function_exists( 'get_filesystem_method' ) ) { if ( ! is_file( $loader ) ) { return null; } require_once $loader; } if ( 'direct' !== get_filesystem_method( [], $context, true ) ) { return null; } require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; // WP_Filesystem() normally defines these; without them core's chmod() // reads an undefined constant and PHP 8 turns that into a fatal. if ( ! defined( 'FS_CHMOD_DIR' ) ) { define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 ) | 0755 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core's own constant, defined exactly as WP_Filesystem() defines it. } if ( ! defined( 'FS_CHMOD_FILE' ) ) { define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 ) | 0644 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core's own constant, defined exactly as WP_Filesystem() defines it. } return new \WP_Filesystem_Direct( null ); } }