blob: d36005456d51a0aa3361b41d025f9dad0c102f4a [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# 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.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Some utility classes for interacting with templates."""
6
7from __future__ import division
8from __future__ import print_function
9from __future__ import absolute_import
10
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010011try:
12 import html
13except ImportError:
14 import cgi as html
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020015from six.moves import http_client
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010016from six.moves import StringIO
Copybara854996b2021-09-07 19:36:02 +000017import logging
18import time
Copybara854996b2021-09-07 19:36:02 +000019
20import ezt
21import six
22
23from protorpc import messages
24
25import settings
26from framework import framework_constants
27
28
29_DISPLAY_VALUE_TRAILING_CHARS = 8
30_DISPLAY_VALUE_TIP_CHARS = 120
31
32
33class 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
108def 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ínezf19ea432024-01-23 20:20:52 +0100123class StringIOUnicodeWrapper(object):
124 """Wrapper on io.StringIO that encodes unicode as UTF-8 as it goes."""
Copybara854996b2021-09-07 19:36:02 +0000125
126 def __init__(self):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100127 self.buffer = StringIO()
Copybara854996b2021-09-07 19:36:02 +0000128
129 def write(self, s):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100130 self.buffer.write(six.ensure_str(s))
Copybara854996b2021-09-07 19:36:02 +0000131
132 def getvalue(self):
133 return self.buffer.getvalue()
134
135
136SNIFFABLE_PATTERNS = {
137 '%PDF-': '%NoNoNo-',
138}
139
140
141class 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ínezde942802022-07-15 14:06:55 +0200157 response.status_code = data.get('http_response_code', http_client.OK)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200158 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ínezde942802022-07-15 14:06:55 +0200163 response.set_data(whole_page)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200164 logging.info('wrote response in %dms', int((time.time() - start) * 1000))
165
Copybara854996b2021-09-07 19:36:02 +0000166 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ínezf19ea432024-01-23 20:20:52 +0100170 buf = StringIOUnicodeWrapper()
Copybara854996b2021-09-07 19:36:02 +0000171 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
208class 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
239def 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
261def 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
274class 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
290def 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ínezf19ea432024-01-23 20:20:52 +0100297 if isinstance(label_list, six.string_types):
Copybara854996b2021-09-07 19:36:02 +0000298 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
306class 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ínezf19ea432024-01-23 20:20:52 +0100320 html.escape(self.href,
321 quote=True), html.escape(self.content, quote=True))
Copybara854996b2021-09-07 19:36:02 +0000322
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100323 return html.escape(self.content, quote=True)