Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2012 The LUCI Authors. All rights reserved. | 1 # Copyright 2012 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed by the Apache v2.0 license that can be | 2 # Use of this source code is governed by the Apache v2.0 license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """This module defines Isolate Server frontend url handlers.""" | 5 """This module defines Isolate Server frontend url handlers.""" |
| 6 | 6 |
| 7 import datetime | 7 import datetime |
| 8 import json | 8 import json |
| 9 import re | |
| 9 | 10 |
| 10 import webapp2 | 11 import webapp2 |
| 11 | 12 |
| 12 from google.appengine.api import memcache | 13 from google.appengine.api import memcache |
| 13 from google.appengine.api import users | 14 from google.appengine.api import users |
| 14 | 15 |
| 15 import acl | 16 import acl |
| 16 import config | 17 import config |
| 17 import gcs | 18 import gcs |
| 18 import mapreduce_jobs | 19 import mapreduce_jobs |
| (...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 144 ### Non-restricted handlers | 145 ### Non-restricted handlers |
| 145 | 146 |
| 146 | 147 |
| 147 class BrowseHandler(auth.AuthenticatingHandler): | 148 class BrowseHandler(auth.AuthenticatingHandler): |
| 148 @auth.autologin | 149 @auth.autologin |
| 149 @auth.require(acl.isolate_readable) | 150 @auth.require(acl.isolate_readable) |
| 150 def get(self): | 151 def get(self): |
| 151 namespace = self.request.get('namespace', 'default-gzip') | 152 namespace = self.request.get('namespace', 'default-gzip') |
| 152 # Support 'hash' for compatibility with old links. To remove eventually. | 153 # Support 'hash' for compatibility with old links. To remove eventually. |
| 153 digest = self.request.get('digest', '') or self.request.get('hash', '') | 154 digest = self.request.get('digest', '') or self.request.get('hash', '') |
| 155 params = { | |
| 156 u'digest': unicode(digest), | |
| 157 u'namespace': unicode(namespace), | |
| 158 u'onload': '', | |
| 159 } | |
| 160 # Check for existence of element, so we can 400/404 | |
| 161 if digest and namespace: | |
| 162 try: | |
| 163 model.get_content(namespace, digest) | |
| 164 except ValueError: | |
| 165 self.abort(400, 'Invalid key') | |
| 166 except LookupError: | |
| 167 self.abort(404, 'Unable to retrieve the entry') | |
|
M-A Ruel
2016/04/14 14:05:06
alignment
kjlubick
2016/04/14 14:57:37
Done.
| |
| 168 self.response.write(template.render('isolate/browse.html', params)) | |
| 169 | |
| 170 | |
| 171 class ContentHandler(auth.AuthenticatingHandler): | |
| 172 @auth.autologin | |
| 173 @auth.require(acl.isolate_readable) | |
| 174 def get(self): | |
| 175 namespace = self.request.get('namespace', 'default-gzip') | |
| 176 digest = self.request.get('digest', '') | |
| 177 | |
| 154 content = None | 178 content = None |
|
M-A Ruel
2016/04/14 14:05:06
This can be removed.
kjlubick
2016/04/14 14:57:37
Done.
| |
| 155 if digest and namespace: | 179 if digest and namespace: |
| 156 # TODO(maruel): Refactor into a function. | 180 raw_data = None |
|
M-A Ruel
2016/04/14 14:05:06
You need to move up above the condition, otherwise
kjlubick
2016/04/14 14:57:37
Good catch. Done.
| |
| 157 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace) | 181 try: |
| 158 if memcache_entry is not None: | 182 raw_data = model.get_content(namespace, digest) |
| 159 raw_data = memcache_entry | 183 except ValueError: |
| 160 else: | 184 self.abort(400, 'Invalid key') |
| 161 try: | 185 except LookupError: |
| 162 key = model.get_entry_key(namespace, digest) | 186 self.abort(404, 'Unable to retrieve the entry') |
| 163 except ValueError: | 187 |
| 164 self.abort(400, 'Invalid key') | |
| 165 entity = key.get() | |
| 166 if entity is None: | |
| 167 self.abort(404, 'Unable to retrieve the entry') | |
| 168 raw_data = entity.content | |
| 169 if not raw_data: | 188 if not raw_data: |
| 189 key = model.get_entry_key(namespace, digest) | |
|
M-A Ruel
2016/04/14 14:05:06
In this case, the entity is effectively loaded twi
kjlubick
2016/04/14 14:57:37
Done. entity is returned as the second argument.
| |
| 170 stream = gcs.read_file(config.settings().gs_bucket, key.id()) | 190 stream = gcs.read_file(config.settings().gs_bucket, key.id()) |
| 171 else: | 191 else: |
| 172 stream = [raw_data] | 192 stream = [raw_data] |
| 173 content = ''.join(model.expand_content(namespace, stream)) | 193 content = ''.join(model.expand_content(namespace, stream)) |
| 194 | |
| 195 self.response.headers['X-Frame-Options'] = 'SAMEORIGIN' | |
| 196 del self.response.headers['Content-Type'] | |
| 197 # Apparently, setting the content type to text/plain encourages the | |
| 198 # browser (Chrome, at least) to sniff the mime type and display | |
| 199 # things like images. Images are autowrapped in <img> and text is | |
| 200 # wrapped in <pre>. | |
| 201 self.response.headers['Content-Type'] = 'text/plain; charset=utf-8' | |
| 202 self.response.headers['Content-Disposition'] = str('filename=%s' % digest) | |
| 174 if content.startswith('{'): | 203 if content.startswith('{'): |
| 175 # Try to format as JSON. | 204 # Try to format as JSON. |
| 176 try: | 205 try: |
| 177 content = json.dumps( | 206 content = json.dumps( |
| 178 json.loads(content), sort_keys=True, indent=2, | 207 json.loads(content), sort_keys=True, indent=2, |
| 179 separators=(',', ': ')) | 208 separators=(',', ': ')) |
| 209 # If we don't wrap this in html, browsers will put content in a pre | |
|
M-A Ruel
2016/04/14 14:05:06
Thanks for the explanation, I didn't know this.
| |
| 210 # tag which is also styled with monospace/pre-wrap. We can't use | |
| 211 # anchor tags in <pre>, so we force it to be a <div>, which happily | |
| 212 # accepts links. | |
| 213 content = ( | |
| 214 '<div style="font-family:monospace;white-space:pre-wrap;">%s</div>' | |
| 215 % content) | |
| 216 # Linkify things that look like hashes | |
| 217 content = re.sub(r'([0-9a-f]{40})', | |
| 218 r'<a target="_blank" href="browse?namespace=default-gzip' + | |
|
M-A Ruel
2016/04/14 14:05:06
use the namespcae instead of hardcoding 'default-g
kjlubick
2016/04/14 14:57:37
Done.
| |
| 219 r'&digest=\1">\1</a>', | |
| 220 content) | |
| 221 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' | |
| 180 except ValueError: | 222 except ValueError: |
| 181 pass | 223 pass |
| 182 content = content.decode('utf8', 'replace') | 224 |
| 183 params = { | 225 self.response.write(content) |
| 184 u'content': content, | |
| 185 u'digest': unicode(digest), | |
| 186 u'namespace': unicode(namespace), | |
| 187 # TODO(maruel): Add back once Web UI authentication is switched to OAuth2. | |
| 188 #'onload': 'update()' if digest else '', | |
| 189 u'onload': '', | |
| 190 } | |
| 191 self.response.write(template.render('isolate/browse.html', params)) | |
| 192 | 226 |
| 193 | 227 |
| 194 class StatsHandler(webapp2.RequestHandler): | 228 class StatsHandler(webapp2.RequestHandler): |
| 195 """Returns the statistics web page.""" | 229 """Returns the statistics web page.""" |
| 196 def get(self): | 230 def get(self): |
| 197 """Presents nice recent statistics. | 231 """Presents nice recent statistics. |
| 198 | 232 |
| 199 It fetches data from the 'JSON' API. | 233 It fetches data from the 'JSON' API. |
| 200 """ | 234 """ |
| 201 # Preloads the data to save a complete request. | 235 # Preloads the data to save a complete request. |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 291 # Administrative urls. | 325 # Administrative urls. |
| 292 webapp2.Route(r'/restricted/config', RestrictedConfigHandler), | 326 webapp2.Route(r'/restricted/config', RestrictedConfigHandler), |
| 293 | 327 |
| 294 # Mapreduce related urls. | 328 # Mapreduce related urls. |
| 295 webapp2.Route( | 329 webapp2.Route( |
| 296 r'/restricted/launch_mapreduce', | 330 r'/restricted/launch_mapreduce', |
| 297 RestrictedLaunchMapReduceJob), | 331 RestrictedLaunchMapReduceJob), |
| 298 | 332 |
| 299 # User web pages. | 333 # User web pages. |
| 300 webapp2.Route(r'/browse', BrowseHandler), | 334 webapp2.Route(r'/browse', BrowseHandler), |
| 335 webapp2.Route(r'/content', ContentHandler), | |
| 301 webapp2.Route(r'/stats', StatsHandler), | 336 webapp2.Route(r'/stats', StatsHandler), |
| 302 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler), | 337 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler), |
| 303 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler), | 338 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler), |
| 304 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler), | 339 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler), |
| 305 webapp2.Route(r'/', RootHandler), | 340 webapp2.Route(r'/', RootHandler), |
| 306 | 341 |
| 307 # AppEngine-specific urls: | 342 # AppEngine-specific urls: |
| 308 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler), | 343 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler), |
| 309 webapp2.Route(r'/_ah/warmup', WarmupHandler), | 344 webapp2.Route(r'/_ah/warmup', WarmupHandler), |
| 310 ] | 345 ] |
| 311 | 346 |
| 312 | 347 |
| 313 def create_application(debug): | 348 def create_application(debug): |
| 314 """Creates the url router. | 349 """Creates the url router. |
| 315 | 350 |
| 316 The basic layouts is as follow: | 351 The basic layouts is as follow: |
| 317 - /restricted/.* requires being an instance administrator. | 352 - /restricted/.* requires being an instance administrator. |
| 318 - /stats/.* has statistics. | 353 - /stats/.* has statistics. |
| 319 """ | 354 """ |
| 320 acl.bootstrap() | 355 acl.bootstrap() |
| 321 template.bootstrap() | 356 template.bootstrap() |
| 322 return webapp2.WSGIApplication(get_routes(), debug=debug) | 357 return webapp2.WSGIApplication(get_routes(), debug=debug) |
| OLD | NEW |