##// END OF EJS Templates
use default_url for logo link
Min RK -
Show More
@@ -1,517 +1,522 b''
1 """Base Tornado handlers for the notebook server."""
1 """Base Tornado handlers for the notebook server."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 from tornado import gen
22 from tornado import gen
23 from tornado.log import app_log
23 from tornado.log import app_log
24
24
25
25
26 import IPython
26 import IPython
27 from IPython.utils.sysinfo import get_sys_info
27 from IPython.utils.sysinfo import get_sys_info
28
28
29 from IPython.config import Application
29 from IPython.config import Application
30 from IPython.utils.path import filefind
30 from IPython.utils.path import filefind
31 from IPython.utils.py3compat import string_types
31 from IPython.utils.py3compat import string_types
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
33
33
34 from IPython.html.services.security import csp_report_uri
34 from IPython.html.services.security import csp_report_uri
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Top-level handlers
37 # Top-level handlers
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40
40
41 sys_info = json.dumps(get_sys_info())
41 sys_info = json.dumps(get_sys_info())
42
42
43 class AuthenticatedHandler(web.RequestHandler):
43 class AuthenticatedHandler(web.RequestHandler):
44 """A RequestHandler with an authenticated user."""
44 """A RequestHandler with an authenticated user."""
45
45
46 def set_default_headers(self):
46 def set_default_headers(self):
47 headers = self.settings.get('headers', {})
47 headers = self.settings.get('headers', {})
48
48
49 if "Content-Security-Policy" not in headers:
49 if "Content-Security-Policy" not in headers:
50 headers["Content-Security-Policy"] = (
50 headers["Content-Security-Policy"] = (
51 "frame-ancestors 'self'; "
51 "frame-ancestors 'self'; "
52 # Make sure the report-uri is relative to the base_url
52 # Make sure the report-uri is relative to the base_url
53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
54 )
54 )
55
55
56 # Allow for overriding headers
56 # Allow for overriding headers
57 for header_name,value in headers.items() :
57 for header_name,value in headers.items() :
58 try:
58 try:
59 self.set_header(header_name, value)
59 self.set_header(header_name, value)
60 except Exception as e:
60 except Exception as e:
61 # tornado raise Exception (not a subclass)
61 # tornado raise Exception (not a subclass)
62 # if method is unsupported (websocket and Access-Control-Allow-Origin
62 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 # for example, so just ignore)
63 # for example, so just ignore)
64 self.log.debug(e)
64 self.log.debug(e)
65
65
66 def clear_login_cookie(self):
66 def clear_login_cookie(self):
67 self.clear_cookie(self.cookie_name)
67 self.clear_cookie(self.cookie_name)
68
68
69 def get_current_user(self):
69 def get_current_user(self):
70 if self.login_handler is None:
70 if self.login_handler is None:
71 return 'anonymous'
71 return 'anonymous'
72 return self.login_handler.get_user(self)
72 return self.login_handler.get_user(self)
73
73
74 @property
74 @property
75 def cookie_name(self):
75 def cookie_name(self):
76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
77 self.request.host
77 self.request.host
78 ))
78 ))
79 return self.settings.get('cookie_name', default_cookie_name)
79 return self.settings.get('cookie_name', default_cookie_name)
80
80
81 @property
81 @property
82 def logged_in(self):
82 def logged_in(self):
83 """Is a user currently logged in?"""
83 """Is a user currently logged in?"""
84 user = self.get_current_user()
84 user = self.get_current_user()
85 return (user and not user == 'anonymous')
85 return (user and not user == 'anonymous')
86
86
87 @property
87 @property
88 def login_handler(self):
88 def login_handler(self):
89 """Return the login handler for this application, if any."""
89 """Return the login handler for this application, if any."""
90 return self.settings.get('login_handler_class', None)
90 return self.settings.get('login_handler_class', None)
91
91
92 @property
92 @property
93 def login_available(self):
93 def login_available(self):
94 """May a user proceed to log in?
94 """May a user proceed to log in?
95
95
96 This returns True if login capability is available, irrespective of
96 This returns True if login capability is available, irrespective of
97 whether the user is already logged in or not.
97 whether the user is already logged in or not.
98
98
99 """
99 """
100 if self.login_handler is None:
100 if self.login_handler is None:
101 return False
101 return False
102 return bool(self.login_handler.login_available(self.settings))
102 return bool(self.login_handler.login_available(self.settings))
103
103
104
104
105 class IPythonHandler(AuthenticatedHandler):
105 class IPythonHandler(AuthenticatedHandler):
106 """IPython-specific extensions to authenticated handling
106 """IPython-specific extensions to authenticated handling
107
107
108 Mostly property shortcuts to IPython-specific settings.
108 Mostly property shortcuts to IPython-specific settings.
109 """
109 """
110
110
111 @property
111 @property
112 def config(self):
112 def config(self):
113 return self.settings.get('config', None)
113 return self.settings.get('config', None)
114
114
115 @property
115 @property
116 def log(self):
116 def log(self):
117 """use the IPython log by default, falling back on tornado's logger"""
117 """use the IPython log by default, falling back on tornado's logger"""
118 if Application.initialized():
118 if Application.initialized():
119 return Application.instance().log
119 return Application.instance().log
120 else:
120 else:
121 return app_log
121 return app_log
122
122
123 #---------------------------------------------------------------
123 #---------------------------------------------------------------
124 # URLs
124 # URLs
125 #---------------------------------------------------------------
125 #---------------------------------------------------------------
126
126
127 @property
127 @property
128 def version_hash(self):
128 def version_hash(self):
129 """The version hash to use for cache hints for static files"""
129 """The version hash to use for cache hints for static files"""
130 return self.settings.get('version_hash', '')
130 return self.settings.get('version_hash', '')
131
131
132 @property
132 @property
133 def mathjax_url(self):
133 def mathjax_url(self):
134 return self.settings.get('mathjax_url', '')
134 return self.settings.get('mathjax_url', '')
135
135
136 @property
136 @property
137 def base_url(self):
137 def base_url(self):
138 return self.settings.get('base_url', '/')
138 return self.settings.get('base_url', '/')
139
139
140 @property
140 @property
141 def default_url(self):
142 return self.settings.get('default_url', '/')
143
144 @property
141 def ws_url(self):
145 def ws_url(self):
142 return self.settings.get('websocket_url', '')
146 return self.settings.get('websocket_url', '')
143
147
144 @property
148 @property
145 def contents_js_source(self):
149 def contents_js_source(self):
146 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
150 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
147 'services/contents'))
151 'services/contents'))
148 return self.settings.get('contents_js_source', 'services/contents')
152 return self.settings.get('contents_js_source', 'services/contents')
149
153
150 #---------------------------------------------------------------
154 #---------------------------------------------------------------
151 # Manager objects
155 # Manager objects
152 #---------------------------------------------------------------
156 #---------------------------------------------------------------
153
157
154 @property
158 @property
155 def kernel_manager(self):
159 def kernel_manager(self):
156 return self.settings['kernel_manager']
160 return self.settings['kernel_manager']
157
161
158 @property
162 @property
159 def contents_manager(self):
163 def contents_manager(self):
160 return self.settings['contents_manager']
164 return self.settings['contents_manager']
161
165
162 @property
166 @property
163 def cluster_manager(self):
167 def cluster_manager(self):
164 return self.settings['cluster_manager']
168 return self.settings['cluster_manager']
165
169
166 @property
170 @property
167 def session_manager(self):
171 def session_manager(self):
168 return self.settings['session_manager']
172 return self.settings['session_manager']
169
173
170 @property
174 @property
171 def terminal_manager(self):
175 def terminal_manager(self):
172 return self.settings['terminal_manager']
176 return self.settings['terminal_manager']
173
177
174 @property
178 @property
175 def kernel_spec_manager(self):
179 def kernel_spec_manager(self):
176 return self.settings['kernel_spec_manager']
180 return self.settings['kernel_spec_manager']
177
181
178 @property
182 @property
179 def config_manager(self):
183 def config_manager(self):
180 return self.settings['config_manager']
184 return self.settings['config_manager']
181
185
182 #---------------------------------------------------------------
186 #---------------------------------------------------------------
183 # CORS
187 # CORS
184 #---------------------------------------------------------------
188 #---------------------------------------------------------------
185
189
186 @property
190 @property
187 def allow_origin(self):
191 def allow_origin(self):
188 """Normal Access-Control-Allow-Origin"""
192 """Normal Access-Control-Allow-Origin"""
189 return self.settings.get('allow_origin', '')
193 return self.settings.get('allow_origin', '')
190
194
191 @property
195 @property
192 def allow_origin_pat(self):
196 def allow_origin_pat(self):
193 """Regular expression version of allow_origin"""
197 """Regular expression version of allow_origin"""
194 return self.settings.get('allow_origin_pat', None)
198 return self.settings.get('allow_origin_pat', None)
195
199
196 @property
200 @property
197 def allow_credentials(self):
201 def allow_credentials(self):
198 """Whether to set Access-Control-Allow-Credentials"""
202 """Whether to set Access-Control-Allow-Credentials"""
199 return self.settings.get('allow_credentials', False)
203 return self.settings.get('allow_credentials', False)
200
204
201 def set_default_headers(self):
205 def set_default_headers(self):
202 """Add CORS headers, if defined"""
206 """Add CORS headers, if defined"""
203 super(IPythonHandler, self).set_default_headers()
207 super(IPythonHandler, self).set_default_headers()
204 if self.allow_origin:
208 if self.allow_origin:
205 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
209 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
206 elif self.allow_origin_pat:
210 elif self.allow_origin_pat:
207 origin = self.get_origin()
211 origin = self.get_origin()
208 if origin and self.allow_origin_pat.match(origin):
212 if origin and self.allow_origin_pat.match(origin):
209 self.set_header("Access-Control-Allow-Origin", origin)
213 self.set_header("Access-Control-Allow-Origin", origin)
210 if self.allow_credentials:
214 if self.allow_credentials:
211 self.set_header("Access-Control-Allow-Credentials", 'true')
215 self.set_header("Access-Control-Allow-Credentials", 'true')
212
216
213 def get_origin(self):
217 def get_origin(self):
214 # Handle WebSocket Origin naming convention differences
218 # Handle WebSocket Origin naming convention differences
215 # The difference between version 8 and 13 is that in 8 the
219 # The difference between version 8 and 13 is that in 8 the
216 # client sends a "Sec-Websocket-Origin" header and in 13 it's
220 # client sends a "Sec-Websocket-Origin" header and in 13 it's
217 # simply "Origin".
221 # simply "Origin".
218 if "Origin" in self.request.headers:
222 if "Origin" in self.request.headers:
219 origin = self.request.headers.get("Origin")
223 origin = self.request.headers.get("Origin")
220 else:
224 else:
221 origin = self.request.headers.get("Sec-Websocket-Origin", None)
225 origin = self.request.headers.get("Sec-Websocket-Origin", None)
222 return origin
226 return origin
223
227
224 #---------------------------------------------------------------
228 #---------------------------------------------------------------
225 # template rendering
229 # template rendering
226 #---------------------------------------------------------------
230 #---------------------------------------------------------------
227
231
228 def get_template(self, name):
232 def get_template(self, name):
229 """Return the jinja template object for a given name"""
233 """Return the jinja template object for a given name"""
230 return self.settings['jinja2_env'].get_template(name)
234 return self.settings['jinja2_env'].get_template(name)
231
235
232 def render_template(self, name, **ns):
236 def render_template(self, name, **ns):
233 ns.update(self.template_namespace)
237 ns.update(self.template_namespace)
234 template = self.get_template(name)
238 template = self.get_template(name)
235 return template.render(**ns)
239 return template.render(**ns)
236
240
237 @property
241 @property
238 def template_namespace(self):
242 def template_namespace(self):
239 return dict(
243 return dict(
240 base_url=self.base_url,
244 base_url=self.base_url,
245 default_url=self.default_url,
241 ws_url=self.ws_url,
246 ws_url=self.ws_url,
242 logged_in=self.logged_in,
247 logged_in=self.logged_in,
243 login_available=self.login_available,
248 login_available=self.login_available,
244 static_url=self.static_url,
249 static_url=self.static_url,
245 sys_info=sys_info,
250 sys_info=sys_info,
246 contents_js_source=self.contents_js_source,
251 contents_js_source=self.contents_js_source,
247 version_hash=self.version_hash,
252 version_hash=self.version_hash,
248 )
253 )
249
254
250 def get_json_body(self):
255 def get_json_body(self):
251 """Return the body of the request as JSON data."""
256 """Return the body of the request as JSON data."""
252 if not self.request.body:
257 if not self.request.body:
253 return None
258 return None
254 # Do we need to call body.decode('utf-8') here?
259 # Do we need to call body.decode('utf-8') here?
255 body = self.request.body.strip().decode(u'utf-8')
260 body = self.request.body.strip().decode(u'utf-8')
256 try:
261 try:
257 model = json.loads(body)
262 model = json.loads(body)
258 except Exception:
263 except Exception:
259 self.log.debug("Bad JSON: %r", body)
264 self.log.debug("Bad JSON: %r", body)
260 self.log.error("Couldn't parse JSON", exc_info=True)
265 self.log.error("Couldn't parse JSON", exc_info=True)
261 raise web.HTTPError(400, u'Invalid JSON in body of request')
266 raise web.HTTPError(400, u'Invalid JSON in body of request')
262 return model
267 return model
263
268
264 def write_error(self, status_code, **kwargs):
269 def write_error(self, status_code, **kwargs):
265 """render custom error pages"""
270 """render custom error pages"""
266 exc_info = kwargs.get('exc_info')
271 exc_info = kwargs.get('exc_info')
267 message = ''
272 message = ''
268 status_message = responses.get(status_code, 'Unknown HTTP Error')
273 status_message = responses.get(status_code, 'Unknown HTTP Error')
269 if exc_info:
274 if exc_info:
270 exception = exc_info[1]
275 exception = exc_info[1]
271 # get the custom message, if defined
276 # get the custom message, if defined
272 try:
277 try:
273 message = exception.log_message % exception.args
278 message = exception.log_message % exception.args
274 except Exception:
279 except Exception:
275 pass
280 pass
276
281
277 # construct the custom reason, if defined
282 # construct the custom reason, if defined
278 reason = getattr(exception, 'reason', '')
283 reason = getattr(exception, 'reason', '')
279 if reason:
284 if reason:
280 status_message = reason
285 status_message = reason
281
286
282 # build template namespace
287 # build template namespace
283 ns = dict(
288 ns = dict(
284 status_code=status_code,
289 status_code=status_code,
285 status_message=status_message,
290 status_message=status_message,
286 message=message,
291 message=message,
287 exception=exception,
292 exception=exception,
288 )
293 )
289
294
290 self.set_header('Content-Type', 'text/html')
295 self.set_header('Content-Type', 'text/html')
291 # render the template
296 # render the template
292 try:
297 try:
293 html = self.render_template('%s.html' % status_code, **ns)
298 html = self.render_template('%s.html' % status_code, **ns)
294 except TemplateNotFound:
299 except TemplateNotFound:
295 self.log.debug("No template for %d", status_code)
300 self.log.debug("No template for %d", status_code)
296 html = self.render_template('error.html', **ns)
301 html = self.render_template('error.html', **ns)
297
302
298 self.write(html)
303 self.write(html)
299
304
300
305
301
306
302 class Template404(IPythonHandler):
307 class Template404(IPythonHandler):
303 """Render our 404 template"""
308 """Render our 404 template"""
304 def prepare(self):
309 def prepare(self):
305 raise web.HTTPError(404)
310 raise web.HTTPError(404)
306
311
307
312
308 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
313 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
309 """static files should only be accessible when logged in"""
314 """static files should only be accessible when logged in"""
310
315
311 @web.authenticated
316 @web.authenticated
312 def get(self, path):
317 def get(self, path):
313 if os.path.splitext(path)[1] == '.ipynb':
318 if os.path.splitext(path)[1] == '.ipynb':
314 name = path.rsplit('/', 1)[-1]
319 name = path.rsplit('/', 1)[-1]
315 self.set_header('Content-Type', 'application/json')
320 self.set_header('Content-Type', 'application/json')
316 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
321 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
317
322
318 return web.StaticFileHandler.get(self, path)
323 return web.StaticFileHandler.get(self, path)
319
324
320 def set_headers(self):
325 def set_headers(self):
321 super(AuthenticatedFileHandler, self).set_headers()
326 super(AuthenticatedFileHandler, self).set_headers()
322 # disable browser caching, rely on 304 replies for savings
327 # disable browser caching, rely on 304 replies for savings
323 if "v" not in self.request.arguments:
328 if "v" not in self.request.arguments:
324 self.add_header("Cache-Control", "no-cache")
329 self.add_header("Cache-Control", "no-cache")
325
330
326 def compute_etag(self):
331 def compute_etag(self):
327 return None
332 return None
328
333
329 def validate_absolute_path(self, root, absolute_path):
334 def validate_absolute_path(self, root, absolute_path):
330 """Validate and return the absolute path.
335 """Validate and return the absolute path.
331
336
332 Requires tornado 3.1
337 Requires tornado 3.1
333
338
334 Adding to tornado's own handling, forbids the serving of hidden files.
339 Adding to tornado's own handling, forbids the serving of hidden files.
335 """
340 """
336 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
341 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
337 abs_root = os.path.abspath(root)
342 abs_root = os.path.abspath(root)
338 if is_hidden(abs_path, abs_root):
343 if is_hidden(abs_path, abs_root):
339 self.log.info("Refusing to serve hidden file, via 404 Error")
344 self.log.info("Refusing to serve hidden file, via 404 Error")
340 raise web.HTTPError(404)
345 raise web.HTTPError(404)
341 return abs_path
346 return abs_path
342
347
343
348
344 def json_errors(method):
349 def json_errors(method):
345 """Decorate methods with this to return GitHub style JSON errors.
350 """Decorate methods with this to return GitHub style JSON errors.
346
351
347 This should be used on any JSON API on any handler method that can raise HTTPErrors.
352 This should be used on any JSON API on any handler method that can raise HTTPErrors.
348
353
349 This will grab the latest HTTPError exception using sys.exc_info
354 This will grab the latest HTTPError exception using sys.exc_info
350 and then:
355 and then:
351
356
352 1. Set the HTTP status code based on the HTTPError
357 1. Set the HTTP status code based on the HTTPError
353 2. Create and return a JSON body with a message field describing
358 2. Create and return a JSON body with a message field describing
354 the error in a human readable form.
359 the error in a human readable form.
355 """
360 """
356 @functools.wraps(method)
361 @functools.wraps(method)
357 @gen.coroutine
362 @gen.coroutine
358 def wrapper(self, *args, **kwargs):
363 def wrapper(self, *args, **kwargs):
359 try:
364 try:
360 result = yield gen.maybe_future(method(self, *args, **kwargs))
365 result = yield gen.maybe_future(method(self, *args, **kwargs))
361 except web.HTTPError as e:
366 except web.HTTPError as e:
362 status = e.status_code
367 status = e.status_code
363 message = e.log_message
368 message = e.log_message
364 self.log.warn(message)
369 self.log.warn(message)
365 self.set_status(e.status_code)
370 self.set_status(e.status_code)
366 reply = dict(message=message, reason=e.reason)
371 reply = dict(message=message, reason=e.reason)
367 self.finish(json.dumps(reply))
372 self.finish(json.dumps(reply))
368 except Exception:
373 except Exception:
369 self.log.error("Unhandled error in API request", exc_info=True)
374 self.log.error("Unhandled error in API request", exc_info=True)
370 status = 500
375 status = 500
371 message = "Unknown server error"
376 message = "Unknown server error"
372 t, value, tb = sys.exc_info()
377 t, value, tb = sys.exc_info()
373 self.set_status(status)
378 self.set_status(status)
374 tb_text = ''.join(traceback.format_exception(t, value, tb))
379 tb_text = ''.join(traceback.format_exception(t, value, tb))
375 reply = dict(message=message, reason=None, traceback=tb_text)
380 reply = dict(message=message, reason=None, traceback=tb_text)
376 self.finish(json.dumps(reply))
381 self.finish(json.dumps(reply))
377 else:
382 else:
378 # FIXME: can use regular return in generators in py3
383 # FIXME: can use regular return in generators in py3
379 raise gen.Return(result)
384 raise gen.Return(result)
380 return wrapper
385 return wrapper
381
386
382
387
383
388
384 #-----------------------------------------------------------------------------
389 #-----------------------------------------------------------------------------
385 # File handler
390 # File handler
386 #-----------------------------------------------------------------------------
391 #-----------------------------------------------------------------------------
387
392
388 # to minimize subclass changes:
393 # to minimize subclass changes:
389 HTTPError = web.HTTPError
394 HTTPError = web.HTTPError
390
395
391 class FileFindHandler(web.StaticFileHandler):
396 class FileFindHandler(web.StaticFileHandler):
392 """subclass of StaticFileHandler for serving files from a search path"""
397 """subclass of StaticFileHandler for serving files from a search path"""
393
398
394 # cache search results, don't search for files more than once
399 # cache search results, don't search for files more than once
395 _static_paths = {}
400 _static_paths = {}
396
401
397 def set_headers(self):
402 def set_headers(self):
398 super(FileFindHandler, self).set_headers()
403 super(FileFindHandler, self).set_headers()
399 # disable browser caching, rely on 304 replies for savings
404 # disable browser caching, rely on 304 replies for savings
400 if "v" not in self.request.arguments or \
405 if "v" not in self.request.arguments or \
401 any(self.request.path.startswith(path) for path in self.no_cache_paths):
406 any(self.request.path.startswith(path) for path in self.no_cache_paths):
402 self.add_header("Cache-Control", "no-cache")
407 self.add_header("Cache-Control", "no-cache")
403
408
404 def initialize(self, path, default_filename=None, no_cache_paths=None):
409 def initialize(self, path, default_filename=None, no_cache_paths=None):
405 self.no_cache_paths = no_cache_paths or []
410 self.no_cache_paths = no_cache_paths or []
406
411
407 if isinstance(path, string_types):
412 if isinstance(path, string_types):
408 path = [path]
413 path = [path]
409
414
410 self.root = tuple(
415 self.root = tuple(
411 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
416 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
412 )
417 )
413 self.default_filename = default_filename
418 self.default_filename = default_filename
414
419
415 def compute_etag(self):
420 def compute_etag(self):
416 return None
421 return None
417
422
418 @classmethod
423 @classmethod
419 def get_absolute_path(cls, roots, path):
424 def get_absolute_path(cls, roots, path):
420 """locate a file to serve on our static file search path"""
425 """locate a file to serve on our static file search path"""
421 with cls._lock:
426 with cls._lock:
422 if path in cls._static_paths:
427 if path in cls._static_paths:
423 return cls._static_paths[path]
428 return cls._static_paths[path]
424 try:
429 try:
425 abspath = os.path.abspath(filefind(path, roots))
430 abspath = os.path.abspath(filefind(path, roots))
426 except IOError:
431 except IOError:
427 # IOError means not found
432 # IOError means not found
428 return ''
433 return ''
429
434
430 cls._static_paths[path] = abspath
435 cls._static_paths[path] = abspath
431 return abspath
436 return abspath
432
437
433 def validate_absolute_path(self, root, absolute_path):
438 def validate_absolute_path(self, root, absolute_path):
434 """check if the file should be served (raises 404, 403, etc.)"""
439 """check if the file should be served (raises 404, 403, etc.)"""
435 if absolute_path == '':
440 if absolute_path == '':
436 raise web.HTTPError(404)
441 raise web.HTTPError(404)
437
442
438 for root in self.root:
443 for root in self.root:
439 if (absolute_path + os.sep).startswith(root):
444 if (absolute_path + os.sep).startswith(root):
440 break
445 break
441
446
442 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
443
448
444
449
445 class ApiVersionHandler(IPythonHandler):
450 class ApiVersionHandler(IPythonHandler):
446
451
447 @json_errors
452 @json_errors
448 def get(self):
453 def get(self):
449 # not authenticated, so give as few info as possible
454 # not authenticated, so give as few info as possible
450 self.finish(json.dumps({"version":IPython.__version__}))
455 self.finish(json.dumps({"version":IPython.__version__}))
451
456
452
457
453 class TrailingSlashHandler(web.RequestHandler):
458 class TrailingSlashHandler(web.RequestHandler):
454 """Simple redirect handler that strips trailing slashes
459 """Simple redirect handler that strips trailing slashes
455
460
456 This should be the first, highest priority handler.
461 This should be the first, highest priority handler.
457 """
462 """
458
463
459 def get(self):
464 def get(self):
460 self.redirect(self.request.uri.rstrip('/'))
465 self.redirect(self.request.uri.rstrip('/'))
461
466
462 post = put = get
467 post = put = get
463
468
464
469
465 class FilesRedirectHandler(IPythonHandler):
470 class FilesRedirectHandler(IPythonHandler):
466 """Handler for redirecting relative URLs to the /files/ handler"""
471 """Handler for redirecting relative URLs to the /files/ handler"""
467
472
468 @staticmethod
473 @staticmethod
469 def redirect_to_files(self, path):
474 def redirect_to_files(self, path):
470 """make redirect logic a reusable static method
475 """make redirect logic a reusable static method
471
476
472 so it can be called from other handlers.
477 so it can be called from other handlers.
473 """
478 """
474 cm = self.contents_manager
479 cm = self.contents_manager
475 if cm.dir_exists(path):
480 if cm.dir_exists(path):
476 # it's a *directory*, redirect to /tree
481 # it's a *directory*, redirect to /tree
477 url = url_path_join(self.base_url, 'tree', path)
482 url = url_path_join(self.base_url, 'tree', path)
478 else:
483 else:
479 orig_path = path
484 orig_path = path
480 # otherwise, redirect to /files
485 # otherwise, redirect to /files
481 parts = path.split('/')
486 parts = path.split('/')
482
487
483 if not cm.file_exists(path=path) and 'files' in parts:
488 if not cm.file_exists(path=path) and 'files' in parts:
484 # redirect without files/ iff it would 404
489 # redirect without files/ iff it would 404
485 # this preserves pre-2.0-style 'files/' links
490 # this preserves pre-2.0-style 'files/' links
486 self.log.warn("Deprecated files/ URL: %s", orig_path)
491 self.log.warn("Deprecated files/ URL: %s", orig_path)
487 parts.remove('files')
492 parts.remove('files')
488 path = '/'.join(parts)
493 path = '/'.join(parts)
489
494
490 if not cm.file_exists(path=path):
495 if not cm.file_exists(path=path):
491 raise web.HTTPError(404)
496 raise web.HTTPError(404)
492
497
493 url = url_path_join(self.base_url, 'files', path)
498 url = url_path_join(self.base_url, 'files', path)
494 url = url_escape(url)
499 url = url_escape(url)
495 self.log.debug("Redirecting %s to %s", self.request.path, url)
500 self.log.debug("Redirecting %s to %s", self.request.path, url)
496 self.redirect(url)
501 self.redirect(url)
497
502
498 def get(self, path=''):
503 def get(self, path=''):
499 return self.redirect_to_files(self, path)
504 return self.redirect_to_files(self, path)
500
505
501
506
502 #-----------------------------------------------------------------------------
507 #-----------------------------------------------------------------------------
503 # URL pattern fragments for re-use
508 # URL pattern fragments for re-use
504 #-----------------------------------------------------------------------------
509 #-----------------------------------------------------------------------------
505
510
506 # path matches any number of `/foo[/bar...]` or just `/` or ''
511 # path matches any number of `/foo[/bar...]` or just `/` or ''
507 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
512 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
508
513
509 #-----------------------------------------------------------------------------
514 #-----------------------------------------------------------------------------
510 # URL to handler mappings
515 # URL to handler mappings
511 #-----------------------------------------------------------------------------
516 #-----------------------------------------------------------------------------
512
517
513
518
514 default_handlers = [
519 default_handlers = [
515 (r".*/", TrailingSlashHandler),
520 (r".*/", TrailingSlashHandler),
516 (r"api", ApiVersionHandler)
521 (r"api", ApiVersionHandler)
517 ]
522 ]
@@ -1,119 +1,119 b''
1 <!DOCTYPE HTML>
1 <!DOCTYPE HTML>
2 <html>
2 <html>
3
3
4 <head>
4 <head>
5 <meta charset="utf-8">
5 <meta charset="utf-8">
6
6
7 <title>{% block title %}IPython Notebook{% endblock %}</title>
7 <title>{% block title %}IPython Notebook{% endblock %}</title>
8 {% block favicon %}<link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
8 {% block favicon %}<link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
9 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
9 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
10 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
10 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
11 <meta name="viewport" content="width=device-width, initial-scale=1.0">
11 <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
12
13 {% block stylesheet %}
13 {% block stylesheet %}
14 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
14 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
15 {% endblock %}
15 {% endblock %}
16 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
16 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
17 <script src="{{static_url("components/es6-promise/promise.min.js")}}" type="text/javascript" charset="utf-8"></script>
17 <script src="{{static_url("components/es6-promise/promise.min.js")}}" type="text/javascript" charset="utf-8"></script>
18 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
18 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
19 <script>
19 <script>
20 require.config({
20 require.config({
21 {% if version_hash %}
21 {% if version_hash %}
22 urlArgs: "v={{version_hash}}",
22 urlArgs: "v={{version_hash}}",
23 {% endif %}
23 {% endif %}
24 baseUrl: '{{static_url("", include_version=False)}}',
24 baseUrl: '{{static_url("", include_version=False)}}',
25 paths: {
25 paths: {
26 nbextensions : '{{ base_url }}nbextensions',
26 nbextensions : '{{ base_url }}nbextensions',
27 kernelspecs : '{{ base_url }}kernelspecs',
27 kernelspecs : '{{ base_url }}kernelspecs',
28 underscore : 'components/underscore/underscore-min',
28 underscore : 'components/underscore/underscore-min',
29 backbone : 'components/backbone/backbone-min',
29 backbone : 'components/backbone/backbone-min',
30 jquery: 'components/jquery/jquery.min',
30 jquery: 'components/jquery/jquery.min',
31 bootstrap: 'components/bootstrap/js/bootstrap.min',
31 bootstrap: 'components/bootstrap/js/bootstrap.min',
32 bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
32 bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
33 jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min',
33 jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min',
34 moment: 'components/moment/moment',
34 moment: 'components/moment/moment',
35 codemirror: 'components/codemirror',
35 codemirror: 'components/codemirror',
36 termjs: 'components/term.js/src/term',
36 termjs: 'components/term.js/src/term',
37 },
37 },
38 shim: {
38 shim: {
39 underscore: {
39 underscore: {
40 exports: '_'
40 exports: '_'
41 },
41 },
42 backbone: {
42 backbone: {
43 deps: ["underscore", "jquery"],
43 deps: ["underscore", "jquery"],
44 exports: "Backbone"
44 exports: "Backbone"
45 },
45 },
46 bootstrap: {
46 bootstrap: {
47 deps: ["jquery"],
47 deps: ["jquery"],
48 exports: "bootstrap"
48 exports: "bootstrap"
49 },
49 },
50 bootstraptour: {
50 bootstraptour: {
51 deps: ["bootstrap"],
51 deps: ["bootstrap"],
52 exports: "Tour"
52 exports: "Tour"
53 },
53 },
54 jqueryui: {
54 jqueryui: {
55 deps: ["jquery"],
55 deps: ["jquery"],
56 exports: "$"
56 exports: "$"
57 }
57 }
58 }
58 }
59 });
59 });
60
60
61 require.config({
61 require.config({
62 map: {
62 map: {
63 '*':{
63 '*':{
64 'contents': '{{ contents_js_source }}',
64 'contents': '{{ contents_js_source }}',
65 }
65 }
66 }
66 }
67 });
67 });
68 </script>
68 </script>
69
69
70 {% block meta %}
70 {% block meta %}
71 {% endblock %}
71 {% endblock %}
72
72
73 </head>
73 </head>
74
74
75 <body class="{% block bodyclasses %}{% endblock %}" {% block params %}{% endblock %}>
75 <body class="{% block bodyclasses %}{% endblock %}" {% block params %}{% endblock %}>
76
76
77 <noscript>
77 <noscript>
78 <div id='noscript'>
78 <div id='noscript'>
79 IPython Notebook requires JavaScript.<br>
79 IPython Notebook requires JavaScript.<br>
80 Please enable it to proceed.
80 Please enable it to proceed.
81 </div>
81 </div>
82 </noscript>
82 </noscript>
83
83
84 <div id="header">
84 <div id="header">
85 <div id="header-container" class="container">
85 <div id="header-container" class="container">
86 <div id="ipython_notebook" class="nav navbar-brand pull-left"><a href="{{base_url}}tree" title='dashboard'>{% block logo %}<img src='{{static_url("base/images/logo.png") }}' alt='Jupyter Notebook'/>{% endblock %}</a></div>
86 <div id="ipython_notebook" class="nav navbar-brand pull-left"><a href="{{default_url}}" title='dashboard'>{% block logo %}<img src='{{static_url("base/images/logo.png") }}' alt='Jupyter Notebook'/>{% endblock %}</a></div>
87
87
88 {% block login_widget %}
88 {% block login_widget %}
89
89
90 <span id="login_widget">
90 <span id="login_widget">
91 {% if logged_in %}
91 {% if logged_in %}
92 <button id="logout" class="btn btn-sm navbar-btn">Logout</button>
92 <button id="logout" class="btn btn-sm navbar-btn">Logout</button>
93 {% elif login_available and not logged_in %}
93 {% elif login_available and not logged_in %}
94 <button id="login" class="btn btn-sm navbar-btn">Login</button>
94 <button id="login" class="btn btn-sm navbar-btn">Login</button>
95 {% endif %}
95 {% endif %}
96 </span>
96 </span>
97
97
98 {% endblock %}
98 {% endblock %}
99
99
100 {% block headercontainer %}
100 {% block headercontainer %}
101 {% endblock %}
101 {% endblock %}
102 </div>
102 </div>
103 <div class="header-bar"></div>
103 <div class="header-bar"></div>
104
104
105 {% block header %}
105 {% block header %}
106 {% endblock %}
106 {% endblock %}
107 </div>
107 </div>
108
108
109 <div id="site">
109 <div id="site">
110 {% block site %}
110 {% block site %}
111 {% endblock %}
111 {% endblock %}
112 </div>
112 </div>
113
113
114 {% block script %}
114 {% block script %}
115 {% endblock %}
115 {% endblock %}
116
116
117 </body>
117 </body>
118
118
119 </html>
119 </html>
General Comments 0
You need to be logged in to leave comments. Login now