blob: 5f383c325cf62f2c03088b2a2d788bb5f0a09255 [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
14import httplib
15import 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
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
169 def GetResponse(self, data):
170 """Generate the text from the template and return it as a string."""
171 template = self.GetTemplate()
172 start = time.time()
173 buf = cStringIOUnicodeWrapper()
174 template.generate(buf, data)
175 whole_page = buf.getvalue()
176 logging.info('rendering took %dms', int((time.time() - start) * 1000))
177 logging.info('whole_page len is %r', len(whole_page))
178 if self.eliminate_blank_lines:
179 lines = whole_page.split('\n')
180 whole_page = '\n'.join(line for line in lines if line.strip())
181 logging.info('smaller whole_page len is %r', len(whole_page))
182 logging.info('smaller rendering took %dms',
183 int((time.time() - start) * 1000))
184 return whole_page
185
186 def GetTemplate(self):
187 """Parse the EZT template, or return an already parsed one."""
188 # We don't operate directly on self.template to avoid races.
189 template = self.template
190
191 if template is None or settings.local_mode:
192 start = time.time()
193 template = ezt.Template(
194 fname=self.template_path,
195 compress_whitespace=self.compress_whitespace,
196 base_format=self.base_format)
197 logging.info('parsed in %dms', int((time.time() - start) * 1000))
198 self.template = template
199
200 return template
201
202 def GetTemplatePath(self):
203 """Accessor for the template path specified in the constructor.
204
205 Returns:
206 The string path for the template file provided to the constructor.
207 """
208 return self.template_path
209
210
211class EZTError(object):
212 """This class is a helper class to pass errors to EZT.
213
214 This class is used to hold information that will be passed to EZT but might
215 be unset. All unset values return None (ie EZT False)
216 Example: page errors
217 """
218
219 def __getattr__(self, _name):
220 """This is the EZT retrieval function."""
221 return None
222
223 def AnyErrors(self):
224 return len(self.__dict__) != 0
225
226 def DebugString(self):
227 return 'EZTError(%s)' % self.__dict__
228
229 def SetError(self, name, value):
230 self.__setattr__(name, value)
231
232 def SetCustomFieldError(self, field_id, value):
233 # This access works because of the custom __getattr__.
234 # pylint: disable=access-member-before-definition
235 # pylint: disable=attribute-defined-outside-init
236 if self.custom_fields is None:
237 self.custom_fields = []
238 self.custom_fields.append(EZTItem(field_id=field_id, message=value))
239
240 any_errors = property(AnyErrors, None)
241
242def FitUnsafeText(text, length):
243 """Trim some unsafe (unescaped) text to a specific length.
244
245 Three periods are appended if trimming occurs. Note that we cannot use
246 the ellipsis character (&hellip) because this is unescaped text.
247
248 Args:
249 text: the string to fit (ASCII or unicode).
250 length: the length to trim to.
251
252 Returns:
253 An ASCII or unicode string fitted to the given length.
254 """
255 if not text:
256 return ""
257
258 if len(text) <= length:
259 return text
260
261 return text[:length] + '...'
262
263
264def BytesKbOrMb(num_bytes):
265 """Return a human-readable string representation of a number of bytes."""
266 if num_bytes < 1024:
267 return '%d bytes' % num_bytes # e.g., 128 bytes
268 if num_bytes < 99 * 1024:
269 return '%.1f KB' % (num_bytes / 1024.0) # e.g. 23.4 KB
270 if num_bytes < 1024 * 1024:
271 return '%d KB' % (num_bytes / 1024) # e.g., 219 KB
272 if num_bytes < 99 * 1024 * 1024:
273 return '%.1f MB' % (num_bytes / 1024.0 / 1024.0) # e.g., 21.9 MB
274 return '%d MB' % (num_bytes / 1024 / 1024) # e.g., 100 MB
275
276
277class EZTItem(object):
278 """A class that makes a collection of fields easily accessible in EZT."""
279
280 def __init__(self, **kwargs):
281 """Store all the given key-value pairs as fields of this object."""
282 vars(self).update(kwargs)
283
284 def __repr__(self):
285 fields = ', '.join('%r: %r' % (k, v) for k, v in
286 sorted(vars(self).items()))
287 return '%s({%s})' % (self.__class__.__name__, fields)
288
289 def __eq__(self, other):
290 return self.__dict__ == other.__dict__
291
292
293def ExpandLabels(page_data):
294 """If page_data has a 'labels' list, expand it into 'label1', etc.
295
296 Args:
297 page_data: Template data which may include a 'labels' field.
298 """
299 label_list = page_data.get('labels', [])
300 if isinstance(label_list, types.StringTypes):
301 label_list = [label.strip() for label in page_data['labels'].split(',')]
302
303 for i in range(len(label_list)):
304 page_data['label%d' % i] = label_list[i]
305 for i in range(len(label_list), framework_constants.MAX_LABELS):
306 page_data['label%d' % i] = ''
307
308
309class TextRun(object):
310 """A fragment of user-entered text that needs to be safely displyed."""
311
312 def __init__(self, content, tag=None, href=None):
313 self.content = content
314 self.tag = tag
315 self.href = href
316 self.title = None
317 self.css_class = None
318
319 def FormatForHTMLEmail(self):
320 """Return a string that can be used in an HTML email body."""
321 if self.tag == 'a' and self.href:
322 return '<a href="%s">%s</a>' % (
323 cgi.escape(self.href, quote=True),
324 cgi.escape(self.content, quote=True))
325
326 return cgi.escape(self.content, quote=True)