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