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