is_dir($cache_root) && ! wp_mkdir_p($cache_root)) { return false; } $bundle_existed = $filesystem->is_dir($bundle_path); if (! $bundle_existed) { if ($filesystem->exists($bundle_path) || ! wp_mkdir_p($bundle_path)) { return false; } } $marker_path = $bundle_path . '/' . self::MARKER_FILE; $expected_marker = self::MARKER_PREFIX . $owner; if (! $filesystem->exists($marker_path)) { // Never claim a pre-existing cache directory that we did not create. if ($bundle_existed) { return false; } if (! $filesystem->put_contents($marker_path, $expected_marker, self::file_permissions())) { return false; } } $marker = $filesystem->get_contents($marker_path); return is_string($marker) && hash_equals($expected_marker, $marker); } /** * Writes an asset atomically inside an owned cache bundle. * * @param string $bundle_key Opaque bundle directory name. * @param string $owner Bundle owner identifier. * @param string $file_name Asset file name. * @param string $contents Asset contents. * @return bool */ public static function write($bundle_key, $owner, $file_name, $contents) { if ( ! is_string($contents) || ! self::is_safe_asset_file_name($file_name) || ! self::ensure_bundle($bundle_key, $owner) ) { return false; } $filesystem = self::get_filesystem(); $target_path = self::get_path($bundle_key, $file_name); $temp_name = $file_name . '.tmp-' . self::generate_opaque_name(6); $temp_path = self::get_path($bundle_key) . '/' . $temp_name; if (! $filesystem || '' === $target_path) { return false; } if (! $filesystem->put_contents($temp_path, $contents, self::file_permissions())) { return false; } if (! $filesystem->move($temp_path, $target_path, true)) { $filesystem->delete($temp_path, false, 'f'); return false; } return $filesystem->is_file($target_path); } /** * Copies a bundled plugin asset into an owned cache bundle. * * @param string $bundle_key Opaque bundle directory name. * @param string $owner Bundle owner identifier. * @param string $file_name Asset file name. * @param string $source_path Absolute source path. * @return bool */ public static function copy($bundle_key, $owner, $file_name, $source_path) { $contents = self::read($source_path); if (! is_string($contents)) { return false; } return self::write($bundle_key, $owner, $file_name, $contents); } /** * Reads a local source file through the WordPress filesystem abstraction. * * @param string $source_path Absolute source path. * @return string|false */ public static function read($source_path) { $filesystem = self::get_filesystem(); if (! $filesystem || ! is_string($source_path) || ! $filesystem->is_file($source_path)) { return false; } $contents = $filesystem->get_contents($source_path); return is_string($contents) ? $contents : false; } /** * Checks that an owned cache bundle contains all requested assets. * * @param string $bundle_key Opaque bundle directory name. * @param string $owner Bundle owner identifier. * @param string[] $file_names Asset file names. * @return bool */ public static function is_ready($bundle_key, $owner, $file_names) { if (! self::has_valid_marker($bundle_key, $owner) || ! is_array($file_names)) { return false; } foreach ($file_names as $file_name) { $path = self::get_path($bundle_key, $file_name); if ('' === $path || ! is_file($path)) { return false; } } return true; } /** * Deletes one asset from an owned cache bundle. * * @param string $bundle_key Opaque bundle directory name. * @param string $owner Bundle owner identifier. * @param string $file_name Asset file name. * @return bool */ public static function delete_file($bundle_key, $owner, $file_name) { if (! self::has_valid_marker($bundle_key, $owner)) { return false; } $filesystem = self::get_filesystem(); $file_path = self::get_path($bundle_key, $file_name); if (! $filesystem || '' === $file_path || ! $filesystem->exists($file_path)) { return false; } return (bool) $filesystem->delete($file_path, false, 'f'); } /** * Deletes an owned cache bundle without touching the shared cache root. * * @param string $bundle_key Opaque bundle directory name. * @param string $owner Bundle owner identifier. * @return bool */ public static function delete_bundle($bundle_key, $owner) { if (! self::has_valid_marker($bundle_key, $owner)) { return false; } $filesystem = self::get_filesystem(); $bundle_path = self::get_path($bundle_key); if (! $filesystem || '' === $bundle_path) { return false; } return (bool) $filesystem->delete($bundle_path, true, 'd'); } /** * Removes a legacy frontend bundle from WP_PLUGIN_DIR after migration. * * Only a directory containing the two exact generated assets is removed. * * @param string $bundle_key Legacy bundle directory and file prefix. * @return bool */ public static function delete_legacy_frontend_bundle($bundle_key) { if (! self::is_safe_bundle_key($bundle_key)) { return false; } $filesystem = self::get_filesystem(); $directory = trailingslashit(WP_PLUGIN_DIR) . $bundle_key; if (! $filesystem || ! $filesystem->is_dir($directory)) { return false; } $allowed_files = array($bundle_key . '.css', $bundle_key . '.js'); $entries = $filesystem->dirlist($directory, false, false); if (! is_array($entries) || empty($entries)) { return false; } foreach ($entries as $file_name => $entry) { if (! in_array($file_name, $allowed_files, true) || ! isset($entry['type']) || 'f' !== $entry['type']) { return false; } } return (bool) $filesystem->delete($directory, true, 'd'); } /** * Removes a legacy marked admin bundle from WP_PLUGIN_DIR after migration. * * @param string $bundle_key Legacy bundle directory name. * @param string $marker_file Legacy marker file name. * @param string $marker_content Expected legacy marker contents. * @return bool */ public static function delete_legacy_marked_bundle($bundle_key, $marker_file, $marker_content) { if ( ! self::is_safe_bundle_key($bundle_key) || ! is_string($marker_file) || wp_basename($marker_file) !== $marker_file || ! is_string($marker_content) ) { return false; } $filesystem = self::get_filesystem(); $directory = trailingslashit(WP_PLUGIN_DIR) . $bundle_key; $marker_path = $directory . '/' . $marker_file; if (! $filesystem || ! $filesystem->is_dir($directory) || ! $filesystem->is_file($marker_path)) { return false; } $marker = $filesystem->get_contents($marker_path); if (! is_string($marker) || ! hash_equals($marker_content, $marker)) { return false; } return (bool) $filesystem->delete($directory, true, 'd'); } /** * Returns whether a bundle key is safe to append to the cache root. * * @param mixed $bundle_key Candidate key. * @return bool */ public static function is_safe_bundle_key($bundle_key) { return is_string($bundle_key) && 1 === preg_match('/^[a-z]{6,32}$/', $bundle_key); } private static function is_safe_asset_file_name($file_name) { return is_string($file_name) && 1 === preg_match('/^[a-z]{6,32}\.(css|js)$/', $file_name); } private static function is_safe_owner($owner) { return is_string($owner) && 1 === preg_match('/^[a-z]{2,16}$/', $owner); } private static function has_valid_marker($bundle_key, $owner) { if (! self::is_safe_bundle_key($bundle_key) || ! self::is_safe_owner($owner)) { return false; } $bundle_path = self::get_path($bundle_key); $marker_path = $bundle_path . '/' . self::MARKER_FILE; if ('' === $bundle_path || ! is_file($marker_path)) { return false; } $marker = file_get_contents($marker_path); return is_string($marker) && hash_equals(self::MARKER_PREFIX . $owner, $marker); } private static function get_filesystem() { if (is_object(self::$filesystem)) { return self::$filesystem; } if (! function_exists('WP_Filesystem')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } global $wp_filesystem; if (! WP_Filesystem() || ! is_object($wp_filesystem)) { return false; } self::$filesystem = $wp_filesystem; return self::$filesystem; } private static function file_permissions() { return defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644; } private static function generate_opaque_name($length) { $characters = 'abcdefghijklmnopqrstuvwxyz'; $name = ''; $max_index = strlen($characters) - 1; for ($index = 0; $index < $length; $index++) { $name .= $characters[random_int(0, $max_index)]; } return $name; } }