Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1 | # Copyright 2016 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Some utility classes for interacting with templates.""" |
| 6 | |
| 7 | from __future__ import division |
| 8 | from __future__ import print_function |
| 9 | from __future__ import absolute_import |
| 10 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 11 | try: |
| 12 | import html |
| 13 | except ImportError: |
| 14 | import cgi as html |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 15 | from six.moves import http_client |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 16 | from six.moves import StringIO |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 17 | import logging |
| 18 | import time |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 19 | |
| 20 | import ezt |
| 21 | import six |
| 22 | |
| 23 | from protorpc import messages |
| 24 | |
| 25 | import settings |
| 26 | from framework import framework_constants |
| 27 | |
| 28 | |
| 29 | _DISPLAY_VALUE_TRAILING_CHARS = 8 |
| 30 | _DISPLAY_VALUE_TIP_CHARS = 120 |
| 31 | |
| 32 | |
| 33 | class PBProxy(object): |
| 34 | """Wraps a Protocol Buffer so it is easy to acceess from a template.""" |
| 35 | |
| 36 | def __init__(self, pb): |
| 37 | self.__pb = pb |
| 38 | |
| 39 | def __getattr__(self, name): |
| 40 | """Make the getters template friendly. |
| 41 | |
| 42 | Psudo-hack alert: When attributes end with _bool, they are converted in |
| 43 | to EZT style bools. I.e., if false return None, if true return True. |
| 44 | |
| 45 | Args: |
| 46 | name: the name of the attribute to get. |
| 47 | |
| 48 | Returns: |
| 49 | The value of that attribute (as an EZT bool if the name ends with _bool). |
| 50 | """ |
| 51 | if name.endswith('_bool'): |
| 52 | bool_name = name |
| 53 | name = name[0:-5] |
| 54 | else: |
| 55 | bool_name = None |
| 56 | |
| 57 | # Make it possible for a PBProxy-local attribute to override the protocol |
| 58 | # buffer field, or even to allow attributes to be added to the PBProxy that |
| 59 | # the protocol buffer does not even have. |
| 60 | if name in self.__dict__: |
| 61 | if callable(self.__dict__[name]): |
| 62 | val = self.__dict__[name]() |
| 63 | else: |
| 64 | val = self.__dict__[name] |
| 65 | |
| 66 | if bool_name: |
| 67 | return ezt.boolean(val) |
| 68 | return val |
| 69 | |
| 70 | if bool_name: |
| 71 | # return an ezt.boolean for the named field. |
| 72 | return ezt.boolean(getattr(self.__pb, name)) |
| 73 | |
| 74 | val = getattr(self.__pb, name) |
| 75 | |
| 76 | if isinstance(val, messages.Enum): |
| 77 | return int(val) # TODO(jrobbins): use str() instead |
| 78 | |
| 79 | if isinstance(val, messages.Message): |
| 80 | return PBProxy(val) |
| 81 | |
| 82 | # Return a list of values whose Message entries |
| 83 | # have been wrapped in PBProxies. |
| 84 | if isinstance(val, (list, messages.FieldList)): |
| 85 | list_to_return = [] |
| 86 | for v in val: |
| 87 | if isinstance(v, messages.Message): |
| 88 | list_to_return.append(PBProxy(v)) |
| 89 | else: |
| 90 | list_to_return.append(v) |
| 91 | return list_to_return |
| 92 | |
| 93 | return val |
| 94 | |
| 95 | def DebugString(self): |
| 96 | """Return a string representation that is useful in debugging.""" |
| 97 | return 'PBProxy(%s)' % self.__pb |
| 98 | |
| 99 | def __eq__(self, other): |
| 100 | # Disable warning about accessing other.__pb. |
| 101 | # pylint: disable=protected-access |
| 102 | return isinstance(other, PBProxy) and self.__pb == other.__pb |
| 103 | |
| 104 | |
| 105 | _templates = {} |
| 106 | |
| 107 | |
| 108 | def GetTemplate( |
| 109 | template_path, compress_whitespace=True, eliminate_blank_lines=False, |
| 110 | base_format=ezt.FORMAT_HTML): |
| 111 | """Make a MonorailTemplate if needed, or reuse one if possible.""" |
| 112 | key = template_path, compress_whitespace, base_format |
| 113 | if key in _templates: |
| 114 | return _templates[key] |
| 115 | |
| 116 | template = MonorailTemplate( |
| 117 | template_path, compress_whitespace=compress_whitespace, |
| 118 | eliminate_blank_lines=eliminate_blank_lines, base_format=base_format) |
| 119 | _templates[key] = template |
| 120 | return template |
| 121 | |
| 122 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 123 | class StringIOUnicodeWrapper(object): |
| 124 | """Wrapper on io.StringIO that encodes unicode as UTF-8 as it goes.""" |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 125 | |
| 126 | def __init__(self): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 127 | self.buffer = StringIO() |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 128 | |
| 129 | def write(self, s): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 130 | self.buffer.write(six.ensure_str(s)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 131 | |
| 132 | def getvalue(self): |
| 133 | return self.buffer.getvalue() |
| 134 | |
| 135 | |
| 136 | SNIFFABLE_PATTERNS = { |
| 137 | '%PDF-': '%NoNoNo-', |
| 138 | } |
| 139 | |
| 140 | |
| 141 | class MonorailTemplate(object): |
| 142 | """A template with additional functionality.""" |
| 143 | |
| 144 | def __init__(self, template_path, compress_whitespace=True, |
| 145 | eliminate_blank_lines=False, base_format=ezt.FORMAT_HTML): |
| 146 | self.template_path = template_path |
| 147 | self.template = None |
| 148 | self.compress_whitespace = compress_whitespace |
| 149 | self.base_format = base_format |
| 150 | self.eliminate_blank_lines = eliminate_blank_lines |
| 151 | |
| 152 | def WriteResponse(self, response, data, content_type=None): |
| 153 | """Write the parsed and filled in template to http server.""" |
| 154 | if content_type: |
| 155 | response.content_type = content_type |
| 156 | |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 157 | response.status_code = data.get('http_response_code', http_client.OK) |
Adrià Vilanova Martínez | ac4a644 | 2022-05-15 19:05:13 +0200 | [diff] [blame] | 158 | whole_page = self.GetResponse(data) |
| 159 | if data.get('prevent_sniffing'): |
| 160 | for sniff_pattern, sniff_replacement in SNIFFABLE_PATTERNS.items(): |
| 161 | whole_page = whole_page.replace(sniff_pattern, sniff_replacement) |
| 162 | start = time.time() |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 163 | response.set_data(whole_page) |
Adrià Vilanova Martínez | ac4a644 | 2022-05-15 19:05:13 +0200 | [diff] [blame] | 164 | logging.info('wrote response in %dms', int((time.time() - start) * 1000)) |
| 165 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 166 | def GetResponse(self, data): |
| 167 | """Generate the text from the template and return it as a string.""" |
| 168 | template = self.GetTemplate() |
| 169 | start = time.time() |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 170 | buf = StringIOUnicodeWrapper() |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 171 | template.generate(buf, data) |
| 172 | whole_page = buf.getvalue() |
| 173 | logging.info('rendering took %dms', int((time.time() - start) * 1000)) |
| 174 | logging.info('whole_page len is %r', len(whole_page)) |
| 175 | if self.eliminate_blank_lines: |
| 176 | lines = whole_page.split('\n') |
| 177 | whole_page = '\n'.join(line for line in lines if line.strip()) |
| 178 | logging.info('smaller whole_page len is %r', len(whole_page)) |
| 179 | logging.info('smaller rendering took %dms', |
| 180 | int((time.time() - start) * 1000)) |
| 181 | return whole_page |
| 182 | |
| 183 | def GetTemplate(self): |
| 184 | """Parse the EZT template, or return an already parsed one.""" |
| 185 | # We don't operate directly on self.template to avoid races. |
| 186 | template = self.template |
| 187 | |
| 188 | if template is None or settings.local_mode: |
| 189 | start = time.time() |
| 190 | template = ezt.Template( |
| 191 | fname=self.template_path, |
| 192 | compress_whitespace=self.compress_whitespace, |
| 193 | base_format=self.base_format) |
| 194 | logging.info('parsed in %dms', int((time.time() - start) * 1000)) |
| 195 | self.template = template |
| 196 | |
| 197 | return template |
| 198 | |
| 199 | def GetTemplatePath(self): |
| 200 | """Accessor for the template path specified in the constructor. |
| 201 | |
| 202 | Returns: |
| 203 | The string path for the template file provided to the constructor. |
| 204 | """ |
| 205 | return self.template_path |
| 206 | |
| 207 | |
| 208 | class EZTError(object): |
| 209 | """This class is a helper class to pass errors to EZT. |
| 210 | |
| 211 | This class is used to hold information that will be passed to EZT but might |
| 212 | be unset. All unset values return None (ie EZT False) |
| 213 | Example: page errors |
| 214 | """ |
| 215 | |
| 216 | def __getattr__(self, _name): |
| 217 | """This is the EZT retrieval function.""" |
| 218 | return None |
| 219 | |
| 220 | def AnyErrors(self): |
| 221 | return len(self.__dict__) != 0 |
| 222 | |
| 223 | def DebugString(self): |
| 224 | return 'EZTError(%s)' % self.__dict__ |
| 225 | |
| 226 | def SetError(self, name, value): |
| 227 | self.__setattr__(name, value) |
| 228 | |
| 229 | def SetCustomFieldError(self, field_id, value): |
| 230 | # This access works because of the custom __getattr__. |
| 231 | # pylint: disable=access-member-before-definition |
| 232 | # pylint: disable=attribute-defined-outside-init |
| 233 | if self.custom_fields is None: |
| 234 | self.custom_fields = [] |
| 235 | self.custom_fields.append(EZTItem(field_id=field_id, message=value)) |
| 236 | |
| 237 | any_errors = property(AnyErrors, None) |
| 238 | |
| 239 | def FitUnsafeText(text, length): |
| 240 | """Trim some unsafe (unescaped) text to a specific length. |
| 241 | |
| 242 | Three periods are appended if trimming occurs. Note that we cannot use |
| 243 | the ellipsis character (&hellip) because this is unescaped text. |
| 244 | |
| 245 | Args: |
| 246 | text: the string to fit (ASCII or unicode). |
| 247 | length: the length to trim to. |
| 248 | |
| 249 | Returns: |
| 250 | An ASCII or unicode string fitted to the given length. |
| 251 | """ |
| 252 | if not text: |
| 253 | return "" |
| 254 | |
| 255 | if len(text) <= length: |
| 256 | return text |
| 257 | |
| 258 | return text[:length] + '...' |
| 259 | |
| 260 | |
| 261 | def BytesKbOrMb(num_bytes): |
| 262 | """Return a human-readable string representation of a number of bytes.""" |
| 263 | if num_bytes < 1024: |
| 264 | return '%d bytes' % num_bytes # e.g., 128 bytes |
| 265 | if num_bytes < 99 * 1024: |
| 266 | return '%.1f KB' % (num_bytes / 1024.0) # e.g. 23.4 KB |
| 267 | if num_bytes < 1024 * 1024: |
| 268 | return '%d KB' % (num_bytes / 1024) # e.g., 219 KB |
| 269 | if num_bytes < 99 * 1024 * 1024: |
| 270 | return '%.1f MB' % (num_bytes / 1024.0 / 1024.0) # e.g., 21.9 MB |
| 271 | return '%d MB' % (num_bytes / 1024 / 1024) # e.g., 100 MB |
| 272 | |
| 273 | |
| 274 | class EZTItem(object): |
| 275 | """A class that makes a collection of fields easily accessible in EZT.""" |
| 276 | |
| 277 | def __init__(self, **kwargs): |
| 278 | """Store all the given key-value pairs as fields of this object.""" |
| 279 | vars(self).update(kwargs) |
| 280 | |
| 281 | def __repr__(self): |
| 282 | fields = ', '.join('%r: %r' % (k, v) for k, v in |
| 283 | sorted(vars(self).items())) |
| 284 | return '%s({%s})' % (self.__class__.__name__, fields) |
| 285 | |
| 286 | def __eq__(self, other): |
| 287 | return self.__dict__ == other.__dict__ |
| 288 | |
| 289 | |
| 290 | def ExpandLabels(page_data): |
| 291 | """If page_data has a 'labels' list, expand it into 'label1', etc. |
| 292 | |
| 293 | Args: |
| 294 | page_data: Template data which may include a 'labels' field. |
| 295 | """ |
| 296 | label_list = page_data.get('labels', []) |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 297 | if isinstance(label_list, six.string_types): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 298 | label_list = [label.strip() for label in page_data['labels'].split(',')] |
| 299 | |
| 300 | for i in range(len(label_list)): |
| 301 | page_data['label%d' % i] = label_list[i] |
| 302 | for i in range(len(label_list), framework_constants.MAX_LABELS): |
| 303 | page_data['label%d' % i] = '' |
| 304 | |
| 305 | |
| 306 | class TextRun(object): |
| 307 | """A fragment of user-entered text that needs to be safely displyed.""" |
| 308 | |
| 309 | def __init__(self, content, tag=None, href=None): |
| 310 | self.content = content |
| 311 | self.tag = tag |
| 312 | self.href = href |
| 313 | self.title = None |
| 314 | self.css_class = None |
| 315 | |
| 316 | def FormatForHTMLEmail(self): |
| 317 | """Return a string that can be used in an HTML email body.""" |
| 318 | if self.tag == 'a' and self.href: |
| 319 | return '<a href="%s">%s</a>' % ( |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 320 | html.escape(self.href, |
| 321 | quote=True), html.escape(self.content, quote=True)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 322 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 323 | return html.escape(self.content, quote=True) |