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