# variable_substitution.py # # Copyright 2025 Pavel Baksy # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later import re from typing import Dict, List, Tuple, Set from .models import HttpRequest, Environment class VariableSubstitution: """Handles variable substitution with {{variable_name}} syntax.""" # Pattern to match {{variable_name}} where variable_name is alphanumeric + underscore VARIABLE_PATTERN = re.compile(r'\{\{(\w+)\}\}') @staticmethod def find_variables(text: str) -> List[str]: """ Extract all {{variable_name}} references from text. Args: text: The text to search for variable references Returns: List of variable names found (without the {{ }} delimiters) """ if not text: return [] return VariableSubstitution.VARIABLE_PATTERN.findall(text) @staticmethod def substitute(text: str, variables: Dict[str, str]) -> Tuple[str, List[str]]: """ Replace {{variable_name}} with values from variables dict. Args: text: The text containing variable placeholders variables: Dictionary mapping variable names to their values Returns: Tuple of (substituted_text, list_of_undefined_variables) """ if not text: return text, [] undefined_vars = [] def replace_var(match): var_name = match.group(1) if var_name in variables: value = variables[var_name] # Check if value is empty/None - treat as undefined for warning purposes if not value: undefined_vars.append(var_name) return "" return value else: # Variable not defined - track it and replace with empty string undefined_vars.append(var_name) return "" substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text) return substituted, undefined_vars @staticmethod def substitute_request(request: HttpRequest, environment: Environment) -> Tuple[HttpRequest, Set[str]]: """ Substitute variables in all request fields (URL, headers, body). Args: request: The HTTP request with variable placeholders environment: The environment containing variable values Returns: Tuple of (new_request_with_substitutions, set_of_undefined_variables) """ all_undefined = set() # Substitute URL new_url, url_undefined = VariableSubstitution.substitute( request.url, environment.variables ) all_undefined.update(url_undefined) # Substitute headers (both keys and values) new_headers = {} for key, value in request.headers.items(): new_key, key_undefined = VariableSubstitution.substitute( key, environment.variables ) new_value, value_undefined = VariableSubstitution.substitute( value, environment.variables ) all_undefined.update(key_undefined) all_undefined.update(value_undefined) new_headers[new_key] = new_value # Substitute body new_body, body_undefined = VariableSubstitution.substitute( request.body, environment.variables ) all_undefined.update(body_undefined) # Create new HttpRequest with substituted values new_request = HttpRequest( method=request.method, url=new_url, headers=new_headers, body=new_body, syntax=request.syntax ) return new_request, all_undefined @staticmethod def substitute_excluding_variables(text: str, variables: Dict[str, str], excluded_vars: List[str]) -> Tuple[str, List[str]]: """ Replace {{variable_name}} with values, but skip excluded variables (leave as {{var}}). Args: text: The text containing variable placeholders variables: Dictionary mapping variable names to their values excluded_vars: List of variable names to skip (leave as placeholders) Returns: Tuple of (substituted_text, list_of_undefined_variables) """ if not text: return text, [] undefined_vars = [] def replace_var(match): var_name = match.group(1) # Skip excluded variables - leave as {{var_name}} if var_name in excluded_vars: return match.group(0) # Return original {{var_name}} # Substitute non-excluded variables if var_name in variables: value = variables[var_name] # Check if value is empty/None - treat as undefined for warning purposes if not value: undefined_vars.append(var_name) return "" return value else: # Variable not defined - track it and replace with empty string undefined_vars.append(var_name) return "" substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text) return substituted, undefined_vars @staticmethod def substitute_request_excluding_sensitive( request: HttpRequest, environment: Environment, sensitive_variables: List[str] ) -> Tuple[HttpRequest, Set[str], bool]: """ Substitute variables in request, but exclude sensitive variables (leave as {{var}}). This is useful for saving requests to history without exposing sensitive values. Sensitive variables remain in their {{variable_name}} placeholder form. Args: request: The HTTP request with variable placeholders environment: The environment containing variable values sensitive_variables: List of variable names to exclude from substitution Returns: Tuple of (redacted_request, set_of_undefined_variables, has_sensitive_vars) - redacted_request: Request with only non-sensitive variables substituted - set_of_undefined_variables: Variables that were referenced but not defined - has_sensitive_vars: True if any sensitive variables were found and redacted """ all_undefined = set() # Substitute URL (excluding sensitive variables) new_url, url_undefined = VariableSubstitution.substitute_excluding_variables( request.url, environment.variables, sensitive_variables ) all_undefined.update(url_undefined) # Substitute headers (both keys and values, excluding sensitive variables) new_headers = {} for key, value in request.headers.items(): new_key, key_undefined = VariableSubstitution.substitute_excluding_variables( key, environment.variables, sensitive_variables ) new_value, value_undefined = VariableSubstitution.substitute_excluding_variables( value, environment.variables, sensitive_variables ) all_undefined.update(key_undefined) all_undefined.update(value_undefined) new_headers[new_key] = new_value # Substitute body (excluding sensitive variables) new_body, body_undefined = VariableSubstitution.substitute_excluding_variables( request.body, environment.variables, sensitive_variables ) all_undefined.update(body_undefined) # Create new HttpRequest with redacted values redacted_request = HttpRequest( method=request.method, url=new_url, headers=new_headers, body=new_body, syntax=request.syntax ) # Check if any sensitive variables were actually used in the request all_vars_in_request = set() all_vars_in_request.update(VariableSubstitution.find_variables(request.url)) all_vars_in_request.update(VariableSubstitution.find_variables(request.body)) for key, value in request.headers.items(): all_vars_in_request.update(VariableSubstitution.find_variables(key)) all_vars_in_request.update(VariableSubstitution.find_variables(value)) # Determine if any sensitive variables were actually present has_sensitive_vars = bool( set(sensitive_variables) & all_vars_in_request ) return redacted_request, all_undefined, has_sensitive_vars