Repository: Automattic/vip-go-mu-plugins
Pull request #6263: Wildcard support for Stream Wrapper Local Passthrough
Author: login: rinatkhaziev
Labels: name: [Status] Deployed to production
Created: 2025-04-22T19:01:47Z | Merged: 2025-04-23T22:02:34Z

## Description

This is taking the initial implementation one step further.

Now we also support wildcard matches. E.g. if we want to ignore any Elementor cache-related file we can easily do so by 

```
VIP_Filesystem_Local_Stream_Wrapper::add_local_file( 'wp-content/uploads/et-cache/* );
```

This is still not 100% ready for prime-time so I'm omitting the changelog. 


## Pre-review checklist

Please make sure the items below have been covered before requesting a review:

- [ ] This change works and has been tested locally or in Codespaces (or has an appropriate fallback).
- [ ] This change works and has been tested on a sandbox.
- [ ] This change has relevant unit tests (if applicable).
- [ ] This change uses a rollout method to ease with deployment (if applicable - especially for large scale actions that require writes).
- [ ] This change has relevant documentation additions / updates (if applicable).
- [ ] I've created a changelog description that aligns with the provided examples.

## Pre-deploy checklist

- [ ] VIP staff: Ensure any alerts added/updated conform to internal standards (see internal documentation). 

## Steps to Test

<!--
Outline the steps to test and verify the PR here.

Example:

1. Check out PR.
1. Go to `wp-admin` > `Tools` > `Bakery`
1. Click on "Bake Cookies" button.
1. Verify cookies are delicious.
-->


Diff:
diff --git a/files/class-vip-filesystem-local-stream-wrapper.php b/files/class-vip-filesystem-local-stream-wrapper.php
index 7ccedcac9e..ac2b49321a 100644
--- a/files/class-vip-filesystem-local-stream-wrapper.php
+++ b/files/class-vip-filesystem-local-stream-wrapper.php
@@ -116,11 +116,18 @@ class VIP_Filesystem_Local_Stream_Wrapper {
 	public static ?API_Client $default_client = null;
 
 	/**
-	 * List of files that should be handled locally
+	 * HashMap for exact filename lookups to provide O(1) performance
 	 *
 	 * @var array
 	 */
-	private static $local_files = [];
+	private static $local_files_map = array();
+
+	/**
+	 * HashMap of wildcard patterns for local files
+	 *
+	 * @var array
+	 */
+	private static $local_file_patterns = array();
 
 	/**
 	 * File handle for local files
@@ -1129,7 +1136,7 @@ private function backtrace_fmt() {
 	/**
 	 * Add a file to the list of files that should be handled locally
 	 *
-	 * @param string $file_path Path to the file
+	 * @param string $file_path Path to the file or a pattern.
 	 * @return bool True if the file was added, false otherwise
 	 */
 	public static function add_local_file( $file_path ) {
@@ -1137,39 +1144,38 @@ public static function add_local_file( $file_path ) {
 			return false;
 		}
 
-		if ( ! in_array( $file_path, static::$local_files, true ) ) {
-			static::$local_files[] = $file_path;
-			return true;
+		// Check if pattern contains wildcards (* or ? or [).
+		$is_pattern = strpbrk( $file_path, '*?[' ) !== false;
+			str_contains( $file_path, '[' );
+
+		if ( $is_pattern ) {
+			static::$local_file_patterns[ $file_path ] = true;
+		} else {
+			static::$local_files_map[ $file_path ] = true;
 		}
 
-		return false;
+		return true;
 	}
 
 	/**
 	 * Remove a file from the list of files that should be handled locally
 	 *
-	 * @param string $file_path Path to the file
+	 * @param string $file_path Path to the file or pattern
 	 * @return bool True if the file was removed, false otherwise
 	 */
 	public static function remove_local_file( $file_path ) {
-		$key = array_search( $file_path, static::$local_files, true );
-
-		if ( false !== $key ) {
-			unset( static::$local_files[ $key ] );
-			static::$local_files = array_values( static::$local_files ); // Reindex array
-			return true;
-		}
-
-		return false;
+		unset( static::$local_file_patterns[ $file_path ] );
+		unset( static::$local_files_map[ $file_path ] );
+		return true;
 	}
 
 	/**
 	 * Get the list of files that should be handled locally
 	 *
-	 * @return array List of file paths
+	 * @return array List of file paths and patterns
 	 */
 	public static function get_local_files() {
-		return static::$local_files;
+		return array_merge( static::$local_files_map, static::$local_file_patterns );
 	}
 
 	/**
@@ -1179,7 +1185,19 @@ public static function get_local_files() {
 	 * @return bool True if the file should be handled locally, false otherwise
 	 */
 	public static function is_local_file( $file_path ) {
-		return in_array( $file_path, static::$local_files, true );
+		// O(1) check for exact matches
+		if ( isset( static::$local_files_map[ $file_path ] ) ) {
+			return true;
+		}
+
+		// Check against wildcard patterns
+		foreach ( static::$local_file_patterns as $pattern => $value ) {
+			if ( fnmatch( $pattern, $file_path, FNM_PATHNAME ) ) {
+				return true;
+			}
+		}
+
+		return false;
 	}
 
 	/**
diff --git a/tests/files/test-vip-filesystem-local-stream-wrapper.php b/tests/files/test-vip-filesystem-local-stream-wrapper.php
index 86ccba5860..0a005dad8a 100644
--- a/tests/files/test-vip-filesystem-local-stream-wrapper.php
+++ b/tests/files/test-vip-filesystem-local-stream-wrapper.php
@@ -312,51 +312,52 @@ public function test_local_files() {
 		$this->stream_wrapper  = new VIP_Filesystem_Local_Stream_Wrapper( $this->api_client_mock );
 		$this->stream_wrapper->register();
 		$this->should_unregister = true;
-		
+
 		// Test adding a file to the local files list
 		$test_file = 'vip://wp-content/uploads/test-local-file.txt';
 		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $test_file ) );
-		
+
 		// Test getting the local files list
 		$local_files = VIP_Filesystem_Local_Stream_Wrapper::get_local_files();
-		$this->assertContains( $test_file, $local_files );
-		
+
+		$this->arrayHasKey( $test_file, $local_files );
+
 		// Test checking if a file is in the local files list
 		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( $test_file ) );
 		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/non-local-file.txt' ) );
-		
+
 		// Test file operations with local files
 		$content = 'Test content for local file';
-		
+
 		// Test writing to a local file
 		$fp = fopen( $test_file, 'w' );
 		$this->assertNotFalse( $fp );
 		$bytes_written = fwrite( $fp, $content );
 		$this->assertEquals( strlen( $content ), $bytes_written );
 		fclose( $fp );
-		
+
 		// Test reading from a local file
 		$fp = fopen( $test_file, 'r' );
 		$this->assertNotFalse( $fp );
 		$read_content = fread( $fp, 1024 );
 		$this->assertEquals( $content, $read_content );
 		fclose( $fp );
-		
+
 		// Test file_get_contents and file_put_contents
 		$this->assertEquals( $content, file_get_contents( $test_file ) );
 		$new_content = 'Updated content';
 		$this->assertNotFalse( file_put_contents( $test_file, $new_content ) );
 		$this->assertEquals( $new_content, file_get_contents( $test_file ) );
-		
+
 		// Test file_exists
 		$this->assertTrue( file_exists( $test_file ) );
-		
+
 		// Test copy
 		$copy_file = 'vip://wp-content/uploads/test-local-file-copy.txt';
 		VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $copy_file );
 		$this->assertTrue( copy( $test_file, $copy_file ) );
 		$this->assertEquals( $new_content, file_get_contents( $copy_file ) );
-		
+
 		// Test rename
 		$rename_file = 'vip://wp-content/uploads/test-local-file-renamed.txt';
 		VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $rename_file );
@@ -364,21 +365,118 @@ public function test_local_files() {
 		$this->assertFalse( file_exists( $copy_file ) );
 		$this->assertTrue( file_exists( $rename_file ) );
 		$this->assertEquals( $new_content, file_get_contents( $rename_file ) );
-		
+
 		// Test unlink
 		$this->assertTrue( unlink( $test_file ) );
 		$this->assertFalse( file_exists( $test_file ) );
 		$this->assertTrue( unlink( $rename_file ) );
 		$this->assertFalse( file_exists( $rename_file ) );
-		
+
 		// Test removing a file from the local files list
 		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::remove_local_file( $test_file ) );
 		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( $test_file ) );
-		
+
 		// Clean up
 		if ( $this->should_unregister ) {
 			stream_wrapper_unregister( VIP_Filesystem_Local_Stream_Wrapper::DEFAULT_PROTOCOL );
 			$this->should_unregister = false;
 		}
 	}
+
+	/**
+	 * Test wildcard pattern matching capabilities
+	 */
+	public function test_wildcard_matching() {
+		// Set up the API client mock
+		$this->api_client_mock = $this->createMock( API_Client::class );
+		$this->stream_wrapper  = new VIP_Filesystem_Local_Stream_Wrapper( $this->api_client_mock );
+		$this->stream_wrapper->register();
+		$this->should_unregister = true;
+
+		// Add a wildcard pattern for image files
+		$image_pattern = 'vip://wp-content/uploads/*.jpg';
+		VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $image_pattern );
+
+		// Test matching against pattern
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/image.jpg' ) );
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/profile.jpg' ) );
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/document.pdf' ) );
+
+		// Add a pattern with question mark wildcard
+		$question_pattern = 'vip://wp-content/uploads/file-?.txt';
+		VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $question_pattern );
+
+		// Test question mark wildcard
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/file-1.txt' ) );
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/file-A.txt' ) );
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/file-12.txt' ) );
+
+		// Add a pattern with character class
+		$char_class_pattern = 'vip://wp-content/uploads/[a-z]*.pdf';
+		VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $char_class_pattern );
+
+		// Test character class pattern
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/document.pdf' ) );
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/abc.pdf' ) );
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/123.pdf' ) );
+
+		// Cleanup
+		VIP_Filesystem_Local_Stream_Wrapper::remove_local_file( $image_pattern );
+		VIP_Filesystem_Local_Stream_Wrapper::remove_local_file( $question_pattern );
+		VIP_Filesystem_Local_Stream_Wrapper::remove_local_file( $char_class_pattern );
+
+		if ( $this->should_unregister ) {
+			stream_wrapper_unregister( VIP_Filesystem_Local_Stream_Wrapper::DEFAULT_PROTOCOL );
+			$this->should_unregister = false;
+		}
+	}
+
+	/**
+	 * Test O(1) lookup optimizations
+	 */
+	public function test_lookup_optimization() {
+		// Set up the API client mock
+		$this->api_client_mock = $this->createMock( API_Client::class );
+		$this->stream_wrapper  = new VIP_Filesystem_Local_Stream_Wrapper( $this->api_client_mock );
+		$this->stream_wrapper->register();
+		$this->should_unregister = true;
+
+		// Clean existing files before testing
+		$existing_files = VIP_Filesystem_Local_Stream_Wrapper::get_local_files();
+		foreach ( $existing_files as $path => $value ) {
+			VIP_Filesystem_Local_Stream_Wrapper::remove_local_file( $path );
+		}
+
+		// First, verify no files are registered
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/exact-file.txt' ) );
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/pattern-123.txt' ) );
+
+		// Add exact file path
+		$exact_path = 'vip://wp-content/uploads/exact-file.txt';
+		VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $exact_path );
+
+		// Verify exact file is now recognized
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( $exact_path ) );
+
+		// Add wildcard pattern
+		$pattern = 'vip://wp-content/uploads/pattern-*.txt';
+		VIP_Filesystem_Local_Stream_Wrapper::add_local_file( $pattern );
+
+		// Verify pattern-matched files are recognized (O(1) lookup optimization)
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/pattern-123.txt' ) );
+		$this->assertTrue( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/pattern-abc.txt' ) );
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/different-123.txt' ) );
+
+		// Test removing files
+		VIP_Filesystem_Local_Stream_Wrapper::remove_local_file( $exact_path );
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( $exact_path ) );
+
+		VIP_Filesystem_Local_Stream_Wrapper::remove_local_file( $pattern );
+		$this->assertFalse( VIP_Filesystem_Local_Stream_Wrapper::is_local_file( 'vip://wp-content/uploads/pattern-123.txt' ) );
+
+		if ( $this->should_unregister ) {
+			stream_wrapper_unregister( VIP_Filesystem_Local_Stream_Wrapper::DEFAULT_PROTOCOL );
+			$this->should_unregister = false;
+		}
+	}
 }


Reviews:
user: login: rebeccahum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T20:01:10Z
user: login: rinatkhaziev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T20:25:21Z
user: login: rebeccahum
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T20:32:13Z

Review comments:
diff_hunk: @@ -1129,47 +1136,45 @@ private function backtrace_fmt() {
 	/**
 	 * Add a file to the list of files that should be handled locally
 	 *
-	 * @param string $file_path Path to the file
+	 * @param string $file_path Path to the file or a pattern.
 	 * @return bool True if the file was added, false otherwise
 	 */
 	public static function add_local_file( $file_path ) {
 		if ( empty( $file_path ) || ! is_string( $file_path ) ) {
 			return false;
 		}
 
-		if ( ! in_array( $file_path, static::$local_files, true ) ) {
-			static::$local_files[] = $file_path;
-			return true;
+		// Check if pattern contains wildcards (* or ? or [).
+		$is_pattern = str_contains( $file_path, '*' ) || str_contains( $file_path, '?' ) ||
path: files/class-vip-filesystem-local-stream-wrapper.php
user: login: rebeccahum
body: ```suggestion
		$is_pattern = strpbrk( $file_path, '*?[' ) !== false;
```
This would be faster I think
created_at: 2025-04-22T20:00:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1129,47 +1136,45 @@ private function backtrace_fmt() {
 	/**
 	 * Add a file to the list of files that should be handled locally
 	 *
-	 * @param string $file_path Path to the file
+	 * @param string $file_path Path to the file or a pattern.
 	 * @return bool True if the file was added, false otherwise
 	 */
 	public static function add_local_file( $file_path ) {
 		if ( empty( $file_path ) || ! is_string( $file_path ) ) {
 			return false;
 		}
 
-		if ( ! in_array( $file_path, static::$local_files, true ) ) {
-			static::$local_files[] = $file_path;
-			return true;
+		// Check if pattern contains wildcards (* or ? or [).
+		$is_pattern = str_contains( $file_path, '*' ) || str_contains( $file_path, '?' ) ||
path: files/class-vip-filesystem-local-stream-wrapper.php
user: login: rinatkhaziev
body: Neat! TIL
created_at: 2025-04-22T20:25:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-22T20:19:46Z
body: ## [Codecov](https://app.codecov.io/gh/Automattic/vip-go-mu-plugins/pull/6263?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Automattic) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 31.51%. Comparing base [(`1cfc421`)](https://app.codecov.io/gh/Automattic/vip-go-mu-plugins/commit/1cfc421d224e0581ed06c8fe080554fab621a191?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Automattic) to head [(`b7bf966`)](https://app.codecov.io/gh/Automattic/vip-go-mu-plugins/commit/b7bf966b8ae0c268efae7f5b277c809ab77b1d50?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Automattic).
> Report is 130 commits behind head on develop.

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##             develop    #6263      +/-   ##
=============================================
+ Coverage      31.49%   31.51%   +0.02%     
- Complexity      5015     5017       +2     
=============================================
  Files            298      298              
  Lines          22046    22050       +4     
=============================================
+ Hits            6944     6950       +6     
+ Misses         15102    15100       -2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Automattic/vip-go-mu-plugins/pull/6263?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Automattic).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Automattic).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-23T21:40:27Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Automattic_vip-go-mu-plugins&pullRequest=6263) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=Automattic_vip-go-mu-plugins&pullRequest=6263&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Automattic_vip-go-mu-plugins&pullRequest=6263&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Automattic_vip-go-mu-plugins&pullRequest=6263&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Automattic_vip-go-mu-plugins&pullRequest=6263&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Automattic_vip-go-mu-plugins&pullRequest=6263&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Automattic_vip-go-mu-plugins&pullRequest=6263)


author_association: NONE