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 # TODO(maruel): Add back once Web UI authentication is switched to OAuth2. | |
|
M-A Ruel
2016/04/13 20:32:09
Remove this while at it, not needed.
kjlubick
2016/04/14 13:18:43
Done.
kjlubick
2016/04/14 13:18:43
Done.
| |
| 159 #'onload': 'update()' if digest else '', | |
| 160 u'onload': '', | |
| 161 } | |
| 162 # Check for existence of element, so we can 400/404 | |
| 163 if digest and namespace: | |
| 164 # TODO(maruel): Refactor into a function. | |
|
M-A Ruel
2016/04/13 20:32:09
Can you make it a function now that it is used at
kjlubick
2016/04/14 13:18:43
I put it in model.py and used it twice here. I co
| |
| 165 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace) | |
| 166 if memcache_entry is None: | |
| 167 try: | |
| 168 key = model.get_entry_key(namespace, digest) | |
| 169 except ValueError: | |
| 170 self.abort(400, 'Invalid key') | |
| 171 entity = key.get() | |
| 172 if entity is None: | |
| 173 self.abort(404, 'Unable to retrieve the entry') | |
| 174 self.response.write(template.render('isolate/browse.html', params)) | |
| 175 | |
|
M-A Ruel
2016/04/13 20:32:09
2 empty lines between file level symbols
kjlubick
2016/04/14 13:18:43
Done.
| |
| 176 class ContentHandler(auth.AuthenticatingHandler): | |
| 177 @auth.autologin | |
| 178 @auth.require(acl.isolate_readable) | |
| 179 def get(self): | |
| 180 namespace = self.request.get('namespace', 'default-gzip') | |
| 181 digest = self.request.get('digest', '') | |
| 182 | |
| 154 content = None | 183 content = None |
| 155 if digest and namespace: | 184 if digest and namespace: |
| 156 # TODO(maruel): Refactor into a function. | 185 # TODO(maruel): Refactor into a function. |
| 157 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace) | 186 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace) |
| 158 if memcache_entry is not None: | 187 if memcache_entry is not None: |
| 159 raw_data = memcache_entry | 188 raw_data = memcache_entry |
| 160 else: | 189 else: |
| 161 try: | 190 try: |
| 162 key = model.get_entry_key(namespace, digest) | 191 key = model.get_entry_key(namespace, digest) |
| 163 except ValueError: | 192 except ValueError: |
| 164 self.abort(400, 'Invalid key') | 193 self.abort(400, 'Invalid key') |
| 165 entity = key.get() | 194 entity = key.get() |
| 166 if entity is None: | 195 if entity is None: |
| 167 self.abort(404, 'Unable to retrieve the entry') | 196 self.abort(404, 'Unable to retrieve the entry') |
| 168 raw_data = entity.content | 197 raw_data = entity.content |
| 169 if not raw_data: | 198 if not raw_data: |
| 170 stream = gcs.read_file(config.settings().gs_bucket, key.id()) | 199 stream = gcs.read_file(config.settings().gs_bucket, key.id()) |
| 171 else: | 200 else: |
| 172 stream = [raw_data] | 201 stream = [raw_data] |
| 173 content = ''.join(model.expand_content(namespace, stream)) | 202 content = ''.join(model.expand_content(namespace, stream)) |
| 203 | |
| 204 self.response.headers['X-Frame-Options'] = 'SAMEORIGIN' | |
| 205 del self.response.headers['Content-Type'] | |
| 206 # Apparently, setting the content type to text/plain encourages the | |
| 207 # browser (Chrome, at least) to sniff the mime type and display | |
| 208 # things like images. Images are autowrapped in <img> and text is | |
| 209 # wrapped in <pre>. | |
| 210 self.response.headers['Content-Type'] = 'text/plain; charset=utf-8' | |
| 211 self.response.headers['Content-Disposition'] = str('filename=%s' % digest) | |
| 174 if content.startswith('{'): | 212 if content.startswith('{'): |
| 175 # Try to format as JSON. | 213 # Try to format as JSON. |
| 176 try: | 214 try: |
| 177 content = json.dumps( | 215 content = json.dumps( |
| 178 json.loads(content), sort_keys=True, indent=2, | 216 json.loads(content), sort_keys=True, indent=2, |
| 179 separators=(',', ': ')) | 217 separators=(',', ': ')) |
| 218 content = ( | |
| 219 '<div style="font-family:monospace;white-space:pre-wrap;">%s</div>' | |
|
M-A Ruel
2016/04/13 20:32:09
Looks like this is not strictly necessary from wha
kjlubick
2016/04/14 13:18:43
I'll add a comment that mirrors this, but I don't
| |
| 220 % content) | |
| 221 # Linkify things that look like hashes | |
| 222 content = re.sub(r'([0-9a-f]{40})', | |
| 223 r"""<a target="_blank" href="browse?namespace=default-gzip | |
| 224 &digest=\1">\1</a>""", | |
| 225 content) | |
| 226 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' | |
| 180 except ValueError: | 227 except ValueError: |
| 181 pass | 228 pass |
| 182 content = content.decode('utf8', 'replace') | 229 |
| 183 params = { | 230 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 | 231 |
| 193 | 232 |
| 194 class StatsHandler(webapp2.RequestHandler): | 233 class StatsHandler(webapp2.RequestHandler): |
| 195 """Returns the statistics web page.""" | 234 """Returns the statistics web page.""" |
| 196 def get(self): | 235 def get(self): |
| 197 """Presents nice recent statistics. | 236 """Presents nice recent statistics. |
| 198 | 237 |
| 199 It fetches data from the 'JSON' API. | 238 It fetches data from the 'JSON' API. |
| 200 """ | 239 """ |
| 201 # Preloads the data to save a complete request. | 240 # Preloads the data to save a complete request. |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 291 # Administrative urls. | 330 # Administrative urls. |
| 292 webapp2.Route(r'/restricted/config', RestrictedConfigHandler), | 331 webapp2.Route(r'/restricted/config', RestrictedConfigHandler), |
| 293 | 332 |
| 294 # Mapreduce related urls. | 333 # Mapreduce related urls. |
| 295 webapp2.Route( | 334 webapp2.Route( |
| 296 r'/restricted/launch_mapreduce', | 335 r'/restricted/launch_mapreduce', |
| 297 RestrictedLaunchMapReduceJob), | 336 RestrictedLaunchMapReduceJob), |
| 298 | 337 |
| 299 # User web pages. | 338 # User web pages. |
| 300 webapp2.Route(r'/browse', BrowseHandler), | 339 webapp2.Route(r'/browse', BrowseHandler), |
| 340 webapp2.Route(r'/content', ContentHandler), | |
| 301 webapp2.Route(r'/stats', StatsHandler), | 341 webapp2.Route(r'/stats', StatsHandler), |
| 302 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler), | 342 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler), |
| 303 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler), | 343 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler), |
| 304 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler), | 344 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler), |
| 305 webapp2.Route(r'/', RootHandler), | 345 webapp2.Route(r'/', RootHandler), |
| 306 | 346 |
| 307 # AppEngine-specific urls: | 347 # AppEngine-specific urls: |
| 308 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler), | 348 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler), |
| 309 webapp2.Route(r'/_ah/warmup', WarmupHandler), | 349 webapp2.Route(r'/_ah/warmup', WarmupHandler), |
| 310 ] | 350 ] |
| 311 | 351 |
| 312 | 352 |
| 313 def create_application(debug): | 353 def create_application(debug): |
| 314 """Creates the url router. | 354 """Creates the url router. |
| 315 | 355 |
| 316 The basic layouts is as follow: | 356 The basic layouts is as follow: |
| 317 - /restricted/.* requires being an instance administrator. | 357 - /restricted/.* requires being an instance administrator. |
| 318 - /stats/.* has statistics. | 358 - /stats/.* has statistics. |
| 319 """ | 359 """ |
| 320 acl.bootstrap() | 360 acl.bootstrap() |
| 321 template.bootstrap() | 361 template.bootstrap() |
| 322 return webapp2.WSGIApplication(get_routes(), debug=debug) | 362 return webapp2.WSGIApplication(get_routes(), debug=debug) |
| OLD | NEW |