diff --git a/src/models.py b/src/models.py
index 75fa0c5..e79cbb6 100644
--- a/src/models.py
+++ b/src/models.py
@@ -80,6 +80,7 @@ class HistoryEntry:
response: Optional[HttpResponse]
error: Optional[str] # Error message if request failed
id: str = None # Unique identifier for the entry
+ has_redacted_variables: bool = False # True if sensitive variables were redacted
def __post_init__(self):
"""Generate UUID if id not provided."""
@@ -94,7 +95,8 @@ class HistoryEntry:
'timestamp': self.timestamp,
'request': self.request.to_dict(),
'response': self.response.to_dict() if self.response else None,
- 'error': self.error
+ 'error': self.error,
+ 'has_redacted_variables': self.has_redacted_variables
}
@classmethod
@@ -105,7 +107,8 @@ class HistoryEntry:
timestamp=data['timestamp'],
request=HttpRequest.from_dict(data['request']),
response=HttpResponse.from_dict(data['response']) if data.get('response') else None,
- error=data.get('error')
+ error=data.get('error'),
+ has_redacted_variables=data.get('has_redacted_variables', False)
)
diff --git a/src/variable_substitution.py b/src/variable_substitution.py
index 16194b3..a5b84e2 100644
--- a/src/variable_substitution.py
+++ b/src/variable_substitution.py
@@ -127,3 +127,118 @@ class VariableSubstitution:
)
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
diff --git a/src/widgets/history-item.ui b/src/widgets/history-item.ui
index de60bd2..d7e9e4f 100644
--- a/src/widgets/history-item.ui
+++ b/src/widgets/history-item.ui
@@ -62,6 +62,19 @@
+
+
+
+
+