• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

testit-tms / adapters-python / 15869211008

25 Jun 2025 06:39AM UTC coverage: 43.078% (+7.9%) from 35.139%
15869211008

Pull #188

github

web-flow
Merge cc17fcbec into bdca47b5c
Pull Request #188: fix: TMS-33524 fix html tags escaping

75 of 106 new or added lines in 3 files covered. (70.75%)

3 existing lines in 1 file now uncovered.

1170 of 2716 relevant lines covered (43.08%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.65
/testit-python-commons/src/testit_python_commons/utils/html_escape_utils.py
1
import os
2✔
2
import re
2✔
3
import logging
2✔
4
from typing import Any, List, Optional, Union
2✔
5
from datetime import datetime, date, time, timedelta
2✔
6
from decimal import Decimal
2✔
7
from uuid import UUID
2✔
8

9

10
class HtmlEscapeUtils:
2✔
11
    """
12
    HTML escape utilities for preventing XSS attacks.
13
    Escapes HTML tags in strings and objects using reflection.
14
    """
15
    
16
    NO_ESCAPE_HTML_ENV_VAR = "NO_ESCAPE_HTML"
2✔
17
    
18
    # Regex patterns to escape only non-escaped characters
19
    # Using negative lookbehind to avoid double escaping
20
    _LESS_THAN_PATTERN = re.compile(r'(?<!\\)<')
2✔
21
    _GREATER_THAN_PATTERN = re.compile(r'(?<!\\)>')
2✔
22
    
23
    @staticmethod
2✔
24
    def escape_html_tags(text: Optional[str]) -> Optional[str]:
2✔
25
        """
26
        Escapes HTML tags to prevent XSS attacks.
27
        Escapes all < as \\< and > as \\> only if they are not already escaped.
28
        Uses regex with negative lookbehind to avoid double escaping.
29
        
30
        Args:
31
            text: The text to escape
32
            
33
        Returns:
34
            Escaped text or original text if escaping is disabled
35
        """
36
        if text is None:
2✔
37
            return None
2✔
38
            
39
        # Check if escaping is disabled via environment variable
40
        no_escape_html = os.environ.get(HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR, "").lower()
2✔
41
        if no_escape_html == "true":
2✔
42
            return text
2✔
43
            
44
        # Use regex with negative lookbehind to escape only non-escaped characters
45
        result = HtmlEscapeUtils._LESS_THAN_PATTERN.sub(r'\\<', text)
2✔
46
        result = HtmlEscapeUtils._GREATER_THAN_PATTERN.sub(r'\\>', result)
2✔
47
        
48
        return result
2✔
49
    
50
    @staticmethod
2✔
51
    def escape_html_in_object(obj: Any) -> Any:
2✔
52
        """
53
        Escapes HTML tags in all string attributes of an object using reflection.
54
        Also processes list attributes: if list of objects - calls escape_html_in_object_list,
55
        if list of strings - escapes each string.
56
        Can be disabled by setting NO_ESCAPE_HTML environment variable to "true".
57
        
58
        Args:
59
            obj: The object to process
60
            
61
        Returns:
62
            The processed object with escaped strings
63
        """
64
        if obj is None:
2✔
NEW
65
            return None
×
66
            
67
        # Check if escaping is disabled via environment variable
68
        no_escape_html = os.environ.get(HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR, "").lower()
2✔
69
        if no_escape_html == "true":
2✔
70
            return obj
2✔
71
            
72
        try:
2✔
73
            HtmlEscapeUtils._process_object_attributes(obj)
2✔
NEW
74
        except Exception as e:
×
75
            # Silently ignore reflection errors
NEW
76
            logging.debug(f"Error processing object attributes: {e}")
×
77
            
78
        return obj
2✔
79
    
80
    @staticmethod
2✔
81
    def escape_html_in_object_list(obj_list: Optional[List[Any]]) -> Optional[List[Any]]:
2✔
82
        """
83
        Escapes HTML tags in all string attributes of objects in a list using reflection.
84
        Can be disabled by setting NO_ESCAPE_HTML environment variable to "true".
85
        
86
        Args:
87
            obj_list: The list of objects to process
88
            
89
        Returns:
90
            The processed list with escaped strings in all objects
91
        """
92
        if obj_list is None:
2✔
NEW
93
            return None
×
94
            
95
        # Check if escaping is disabled via environment variable
96
        no_escape_html = os.environ.get(HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR, "").lower()
2✔
97
        if no_escape_html == "true":
2✔
NEW
98
            return obj_list
×
99
            
100
        for obj in obj_list:
2✔
101
            HtmlEscapeUtils.escape_html_in_object(obj)
2✔
102
            
103
        return obj_list
2✔
104
    
105
    @staticmethod
2✔
106
    def _process_object_attributes(obj: Any) -> None:
2✔
107
        """
108
        Process all attributes of an object for HTML escaping.
109
        """
110
        # Handle dictionary-like objects (common in API models)
111
        if hasattr(obj, '__dict__'):
2✔
112
            for attr_name in dir(obj):
2✔
113
                # Skip private/protected attributes and methods
114
                if attr_name.startswith('_') or callable(getattr(obj, attr_name, None)):
2✔
115
                    continue
2✔
116
                    
117
                try:
2✔
118
                    value = getattr(obj, attr_name)
2✔
119
                    HtmlEscapeUtils._process_attribute_value(obj, attr_name, value)
2✔
NEW
120
                except Exception as e:
×
121
                    # Silently ignore attribute errors
NEW
122
                    logging.debug(f"Error processing attribute {attr_name}: {e}")
×
123
                    
124
        # Handle dictionary objects
125
        elif isinstance(obj, dict):
2✔
126
            for key, value in obj.items():
2✔
127
                if isinstance(value, str):
2✔
128
                    obj[key] = HtmlEscapeUtils.escape_html_tags(value)
2✔
129
                elif isinstance(value, list):
2✔
130
                    HtmlEscapeUtils._process_list(value)
2✔
NEW
131
                elif not HtmlEscapeUtils._is_simple_type(type(value)):
×
NEW
132
                    HtmlEscapeUtils.escape_html_in_object(value)
×
133
    
134
    @staticmethod
2✔
135
    def _process_attribute_value(obj: Any, attr_name: str, value: Any) -> None:
2✔
136
        """
137
        Process a single attribute value for HTML escaping.
138
        """
139
        if isinstance(value, str):
2✔
140
            # Escape string attributes
141
            try:
2✔
142
                setattr(obj, attr_name, HtmlEscapeUtils.escape_html_tags(value))
2✔
NEW
143
            except AttributeError:
×
144
                # Attribute might be read-only
NEW
145
                pass
×
146
        elif isinstance(value, list):
2✔
147
            HtmlEscapeUtils._process_list(value)
2✔
NEW
148
        elif value is not None and not HtmlEscapeUtils._is_simple_type(type(value)):
×
149
            # Process nested objects (but not simple types)
NEW
150
            HtmlEscapeUtils.escape_html_in_object(value)
×
151
    
152
    @staticmethod
2✔
153
    def _process_list(lst: List[Any]) -> None:
2✔
154
        """
155
        Process a list for HTML escaping.
156
        """
157
        if not lst:
2✔
NEW
158
            return
×
159
            
160
        first_element = lst[0]
2✔
161
        
162
        if isinstance(first_element, str):
2✔
163
            # List of strings - escape each string
164
            for i, item in enumerate(lst):
2✔
165
                if isinstance(item, str):
2✔
166
                    lst[i] = HtmlEscapeUtils.escape_html_tags(item)
2✔
NEW
167
        elif first_element is not None:
×
168
            # List of objects - process each object
NEW
169
            for item in lst:
×
NEW
170
                HtmlEscapeUtils.escape_html_in_object(item)
×
171
    
172
    @staticmethod
2✔
173
    def _is_simple_type(obj_type: type) -> bool:
2✔
174
        """
175
        Checks if a type is a simple type that doesn't need HTML escaping.
176
        
177
        Args:
178
            obj_type: Type to check
179
            
180
        Returns:
181
            True if it's a simple type
182
        """
NEW
183
        simple_types = {
×
184
            # Basic types
185
            bool, int, float, complex, bytes, bytearray,
186
            # String type (handled separately)
187
            str,
188
            # Date/time types
189
            datetime, date, time, timedelta,
190
            # Other common types
191
            Decimal, UUID,
192
            # None type
193
            type(None)
194
        }
195
        
NEW
196
        return (
×
197
            obj_type in simple_types or
198
            # Check for enums
199
            (hasattr(obj_type, '__bases__') and any(
200
                base.__name__ == 'Enum' for base in obj_type.__bases__
201
            ))
202
        ) 
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc