blob: 7ebb7e2067066f6d868950ece5af9d2e8ca76565 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# 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
8from __future__ import division
9from __future__ import print_function
10from __future__ import absolute_import
11
12import cgi
13import cStringIO
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020014from six.moves import http_client
Copybara854996b2021-09-07 19:36:02 +000015import logging
16import time
17import types
18
19import ezt
20import six
21
22from protorpc import messages
23
24import settings
25from framework import framework_constants
26
27
28_DISPLAY_VALUE_TRAILING_CHARS = 8
29_DISPLAY_VALUE_TIP_CHARS = 120
30
31
32class 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
107def 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
122class 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
139SNIFFABLE_PATTERNS = {
140 '%PDF-': '%NoNoNo-',
141}
142
143
144class 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
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200160 response.status = data.get('http_response_code', http_client.OK)
Copybara854996b2021-09-07 19:36:02 +0000161 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ínezac4a6442022-05-15 19:05:13 +0200169 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
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200174 response.status_code = data.get('http_response_code', http_client.OK)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200175 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()
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200180 response.set_data(whole_page)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200181 logging.info('wrote response in %dms', int((time.time() - start) * 1000))
182
Copybara854996b2021-09-07 19:36:02 +0000183 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
225class 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
256def 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
278def 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
291class 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
307def 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
323class 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)