$folder . '/', DataDirectory::reserved_folders() ), // A network keeps each site's uploads under sites/. ...array_map( static fn( string $folder ): string => 'sites/[0-9]*/' . $folder . '/*', DataDirectory::reserved_folders() ), ]; } /** * @param string $pattern Shell-style pattern. * @param string $relative File path relative to the base, using forward slashes. */ public function matches( string $pattern, string $relative ): bool { $pattern = trim( $pattern ); $relative = str_replace( '\\', '/', $relative ); if ( '' === $pattern || '' === $relative ) { return false; } if ( fnmatch( $pattern, $relative ) ) { return true; } // fnmatch would match "uploads/private" itself but nothing inside it, so check the prefix too. if ( str_contains( $pattern, '/' ) ) { if ( str_starts_with( $relative, rtrim( $pattern, '/' ) . '/' ) ) { return true; } // The pattern may name a file; compare the basename so it matches wherever the file sits. return fnmatch( $pattern, basename( $relative ) ); } // No separator: test every segment, so "node_modules" and "*.log" match at any depth. foreach ( explode( '/', $relative ) as $segment ) { if ( fnmatch( $pattern, $segment ) ) { return true; } } return false; } /** * @param string[] $patterns */ public function any_match( array $patterns, string $relative ): bool { foreach ( $patterns as $pattern ) { if ( $this->matches( (string) $pattern, $relative ) ) { return true; } } return false; } }