##// END OF EJS Templates
Merge pull request #2218 from Carreau/new_redirect...
Brian E. Granger -
r8189:1ad91e6b merge
parent child Browse files
Show More
@@ -1,920 +1,908 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import Cookie
19 import Cookie
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import hashlib
22 import hashlib
23 import logging
23 import logging
24 import mimetypes
24 import mimetypes
25 import os
25 import os
26 import stat
26 import stat
27 import threading
27 import threading
28 import time
28 import time
29 import uuid
29 import uuid
30
30
31 from tornado import web
31 from tornado import web
32 from tornado import websocket
32 from tornado import websocket
33
33
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 from zmq.utils import jsonapi
35 from zmq.utils import jsonapi
36
36
37 from IPython.external.decorator import decorator
37 from IPython.external.decorator import decorator
38 from IPython.zmq.session import Session
38 from IPython.zmq.session import Session
39 from IPython.lib.security import passwd_check
39 from IPython.lib.security import passwd_check
40 from IPython.utils.jsonutil import date_default
40 from IPython.utils.jsonutil import date_default
41 from IPython.utils.path import filefind
41 from IPython.utils.path import filefind
42
42
43 try:
43 try:
44 from docutils.core import publish_string
44 from docutils.core import publish_string
45 except ImportError:
45 except ImportError:
46 publish_string = None
46 publish_string = None
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
49 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 # Google Chrome, as of release 16, changed its websocket protocol number. The
52 # Google Chrome, as of release 16, changed its websocket protocol number. The
53 # parts tornado cares about haven't really changed, so it's OK to continue
53 # parts tornado cares about haven't really changed, so it's OK to continue
54 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
54 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
55 # version as of Oct 30/2011) the version check fails, see the issue report:
55 # version as of Oct 30/2011) the version check fails, see the issue report:
56
56
57 # https://github.com/facebook/tornado/issues/385
57 # https://github.com/facebook/tornado/issues/385
58
58
59 # This issue has been fixed in Tornado post 2.1.1:
59 # This issue has been fixed in Tornado post 2.1.1:
60
60
61 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
61 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
62
62
63 # Here we manually apply the same patch as above so that users of IPython can
63 # Here we manually apply the same patch as above so that users of IPython can
64 # continue to work with an officially released Tornado. We make the
64 # continue to work with an officially released Tornado. We make the
65 # monkeypatch version check as narrow as possible to limit its effects; once
65 # monkeypatch version check as narrow as possible to limit its effects; once
66 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
66 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
67
67
68 import tornado
68 import tornado
69
69
70 if tornado.version_info <= (2,1,1):
70 if tornado.version_info <= (2,1,1):
71
71
72 def _execute(self, transforms, *args, **kwargs):
72 def _execute(self, transforms, *args, **kwargs):
73 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
73 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
74
74
75 self.open_args = args
75 self.open_args = args
76 self.open_kwargs = kwargs
76 self.open_kwargs = kwargs
77
77
78 # The difference between version 8 and 13 is that in 8 the
78 # The difference between version 8 and 13 is that in 8 the
79 # client sends a "Sec-Websocket-Origin" header and in 13 it's
79 # client sends a "Sec-Websocket-Origin" header and in 13 it's
80 # simply "Origin".
80 # simply "Origin".
81 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
81 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
82 self.ws_connection = WebSocketProtocol8(self)
82 self.ws_connection = WebSocketProtocol8(self)
83 self.ws_connection.accept_connection()
83 self.ws_connection.accept_connection()
84
84
85 elif self.request.headers.get("Sec-WebSocket-Version"):
85 elif self.request.headers.get("Sec-WebSocket-Version"):
86 self.stream.write(tornado.escape.utf8(
86 self.stream.write(tornado.escape.utf8(
87 "HTTP/1.1 426 Upgrade Required\r\n"
87 "HTTP/1.1 426 Upgrade Required\r\n"
88 "Sec-WebSocket-Version: 8\r\n\r\n"))
88 "Sec-WebSocket-Version: 8\r\n\r\n"))
89 self.stream.close()
89 self.stream.close()
90
90
91 else:
91 else:
92 self.ws_connection = WebSocketProtocol76(self)
92 self.ws_connection = WebSocketProtocol76(self)
93 self.ws_connection.accept_connection()
93 self.ws_connection.accept_connection()
94
94
95 websocket.WebSocketHandler._execute = _execute
95 websocket.WebSocketHandler._execute = _execute
96 del _execute
96 del _execute
97
97
98 #-----------------------------------------------------------------------------
98 #-----------------------------------------------------------------------------
99 # Decorator for disabling read-only handlers
99 # Decorator for disabling read-only handlers
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101
101
102 @decorator
102 @decorator
103 def not_if_readonly(f, self, *args, **kwargs):
103 def not_if_readonly(f, self, *args, **kwargs):
104 if self.application.read_only:
104 if self.application.read_only:
105 raise web.HTTPError(403, "Notebook server is read-only")
105 raise web.HTTPError(403, "Notebook server is read-only")
106 else:
106 else:
107 return f(self, *args, **kwargs)
107 return f(self, *args, **kwargs)
108
108
109 @decorator
109 @decorator
110 def authenticate_unless_readonly(f, self, *args, **kwargs):
110 def authenticate_unless_readonly(f, self, *args, **kwargs):
111 """authenticate this page *unless* readonly view is active.
111 """authenticate this page *unless* readonly view is active.
112
112
113 In read-only mode, the notebook list and print view should
113 In read-only mode, the notebook list and print view should
114 be accessible without authentication.
114 be accessible without authentication.
115 """
115 """
116
116
117 @web.authenticated
117 @web.authenticated
118 def auth_f(self, *args, **kwargs):
118 def auth_f(self, *args, **kwargs):
119 return f(self, *args, **kwargs)
119 return f(self, *args, **kwargs)
120
120
121 if self.application.read_only:
121 if self.application.read_only:
122 return f(self, *args, **kwargs)
122 return f(self, *args, **kwargs)
123 else:
123 else:
124 return auth_f(self, *args, **kwargs)
124 return auth_f(self, *args, **kwargs)
125
125
126 def urljoin(*pieces):
127 """Join componenet of url into a relative url
128
129 Use to prevent double slash when joining subpath
130 """
131 striped = [s.strip('/') for s in pieces]
132 return '/'.join(s for s in striped if s)
133
126 #-----------------------------------------------------------------------------
134 #-----------------------------------------------------------------------------
127 # Top-level handlers
135 # Top-level handlers
128 #-----------------------------------------------------------------------------
136 #-----------------------------------------------------------------------------
129
137
130 class RequestHandler(web.RequestHandler):
138 class RequestHandler(web.RequestHandler):
131 """RequestHandler with default variable setting."""
139 """RequestHandler with default variable setting."""
132
140
133 def render(*args, **kwargs):
141 def render(*args, **kwargs):
134 kwargs.setdefault('message', '')
142 kwargs.setdefault('message', '')
135 return web.RequestHandler.render(*args, **kwargs)
143 return web.RequestHandler.render(*args, **kwargs)
136
144
137 class AuthenticatedHandler(RequestHandler):
145 class AuthenticatedHandler(RequestHandler):
138 """A RequestHandler with an authenticated user."""
146 """A RequestHandler with an authenticated user."""
139
147
140 def get_current_user(self):
148 def get_current_user(self):
141 user_id = self.get_secure_cookie("username")
149 user_id = self.get_secure_cookie("username")
142 # For now the user_id should not return empty, but it could eventually
150 # For now the user_id should not return empty, but it could eventually
143 if user_id == '':
151 if user_id == '':
144 user_id = 'anonymous'
152 user_id = 'anonymous'
145 if user_id is None:
153 if user_id is None:
146 # prevent extra Invalid cookie sig warnings:
154 # prevent extra Invalid cookie sig warnings:
147 self.clear_cookie('username')
155 self.clear_cookie('username')
148 if not self.application.password and not self.application.read_only:
156 if not self.application.password and not self.application.read_only:
149 user_id = 'anonymous'
157 user_id = 'anonymous'
150 return user_id
158 return user_id
151
159
152 @property
160 @property
153 def logged_in(self):
161 def logged_in(self):
154 """Is a user currently logged in?
162 """Is a user currently logged in?
155
163
156 """
164 """
157 user = self.get_current_user()
165 user = self.get_current_user()
158 return (user and not user == 'anonymous')
166 return (user and not user == 'anonymous')
159
167
160 @property
168 @property
161 def login_available(self):
169 def login_available(self):
162 """May a user proceed to log in?
170 """May a user proceed to log in?
163
171
164 This returns True if login capability is available, irrespective of
172 This returns True if login capability is available, irrespective of
165 whether the user is already logged in or not.
173 whether the user is already logged in or not.
166
174
167 """
175 """
168 return bool(self.application.password)
176 return bool(self.application.password)
169
177
170 @property
178 @property
171 def read_only(self):
179 def read_only(self):
172 """Is the notebook read-only?
180 """Is the notebook read-only?
173
181
174 """
182 """
175 return self.application.read_only
183 return self.application.read_only
176
184
177 @property
185 @property
178 def ws_url(self):
186 def ws_url(self):
179 """websocket url matching the current request
187 """websocket url matching the current request
180
188
181 turns http[s]://host[:port] into
189 turns http[s]://host[:port] into
182 ws[s]://host[:port]
190 ws[s]://host[:port]
183 """
191 """
184 proto = self.request.protocol.replace('http', 'ws')
192 proto = self.request.protocol.replace('http', 'ws')
185 host = self.application.ipython_app.websocket_host # default to config value
193 host = self.application.ipython_app.websocket_host # default to config value
186 if host == '':
194 if host == '':
187 host = self.request.host # get from request
195 host = self.request.host # get from request
188 return "%s://%s" % (proto, host)
196 return "%s://%s" % (proto, host)
189
197
190
198
191 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
199 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
192 """static files should only be accessible when logged in"""
200 """static files should only be accessible when logged in"""
193
201
194 @authenticate_unless_readonly
202 @authenticate_unless_readonly
195 def get(self, path):
203 def get(self, path):
196 return web.StaticFileHandler.get(self, path)
204 return web.StaticFileHandler.get(self, path)
197
205
198
206
199 class ProjectDashboardHandler(AuthenticatedHandler):
207 class ProjectDashboardHandler(AuthenticatedHandler):
200
208
201 @authenticate_unless_readonly
209 @authenticate_unless_readonly
202 def get(self):
210 def get(self):
203 nbm = self.application.notebook_manager
211 nbm = self.application.notebook_manager
204 project = nbm.notebook_dir
212 project = nbm.notebook_dir
205 self.render(
213 self.render(
206 'projectdashboard.html', project=project,
214 'projectdashboard.html', project=project,
207 base_project_url=self.application.ipython_app.base_project_url,
215 base_project_url=self.application.ipython_app.base_project_url,
208 base_kernel_url=self.application.ipython_app.base_kernel_url,
216 base_kernel_url=self.application.ipython_app.base_kernel_url,
209 read_only=self.read_only,
217 read_only=self.read_only,
210 logged_in=self.logged_in,
218 logged_in=self.logged_in,
211 login_available=self.login_available
219 login_available=self.login_available
212 )
220 )
213
221
214
222
215 class LoginHandler(AuthenticatedHandler):
223 class LoginHandler(AuthenticatedHandler):
216
224
217 def _render(self, message=None):
225 def _render(self, message=None):
218 self.render('login.html',
226 self.render('login.html',
219 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
227 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
220 read_only=self.read_only,
228 read_only=self.read_only,
221 logged_in=self.logged_in,
229 logged_in=self.logged_in,
222 login_available=self.login_available,
230 login_available=self.login_available,
223 base_project_url=self.application.ipython_app.base_project_url,
231 base_project_url=self.application.ipython_app.base_project_url,
224 message=message
232 message=message
225 )
233 )
226
234
227 def get(self):
235 def get(self):
228 if self.current_user:
236 if self.current_user:
229 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
237 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
230 else:
238 else:
231 self._render()
239 self._render()
232
240
233 def post(self):
241 def post(self):
234 pwd = self.get_argument('password', default=u'')
242 pwd = self.get_argument('password', default=u'')
235 if self.application.password:
243 if self.application.password:
236 if passwd_check(self.application.password, pwd):
244 if passwd_check(self.application.password, pwd):
237 self.set_secure_cookie('username', str(uuid.uuid4()))
245 self.set_secure_cookie('username', str(uuid.uuid4()))
238 else:
246 else:
239 self._render(message={'error': 'Invalid password'})
247 self._render(message={'error': 'Invalid password'})
240 return
248 return
241
249
242 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
250 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
243
251
244
252
245 class LogoutHandler(AuthenticatedHandler):
253 class LogoutHandler(AuthenticatedHandler):
246
254
247 def get(self):
255 def get(self):
248 self.clear_cookie('username')
256 self.clear_cookie('username')
249 if self.login_available:
257 if self.login_available:
250 message = {'info': 'Successfully logged out.'}
258 message = {'info': 'Successfully logged out.'}
251 else:
259 else:
252 message = {'warning': 'Cannot log out. Notebook authentication '
260 message = {'warning': 'Cannot log out. Notebook authentication '
253 'is disabled.'}
261 'is disabled.'}
254
262
255 self.render('logout.html',
263 self.render('logout.html',
256 read_only=self.read_only,
264 read_only=self.read_only,
257 logged_in=self.logged_in,
265 logged_in=self.logged_in,
258 login_available=self.login_available,
266 login_available=self.login_available,
259 base_project_url=self.application.ipython_app.base_project_url,
267 base_project_url=self.application.ipython_app.base_project_url,
260 message=message)
268 message=message)
261
269
262
270
263 class NewHandler(AuthenticatedHandler):
271 class NewHandler(AuthenticatedHandler):
264
272
265 @web.authenticated
273 @web.authenticated
266 def get(self):
274 def get(self):
267 nbm = self.application.notebook_manager
275 nbm = self.application.notebook_manager
268 project = nbm.notebook_dir
276 project = nbm.notebook_dir
269 notebook_id = nbm.new_notebook()
277 notebook_id = nbm.new_notebook()
270 self.render(
278 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
271 'notebook.html', project=project,
272 notebook_id=notebook_id,
273 base_project_url=self.application.ipython_app.base_project_url,
274 base_kernel_url=self.application.ipython_app.base_kernel_url,
275 kill_kernel=False,
276 read_only=False,
277 logged_in=self.logged_in,
278 login_available=self.login_available,
279 mathjax_url=self.application.ipython_app.mathjax_url,
280 )
281
279
282
280
283 class NamedNotebookHandler(AuthenticatedHandler):
281 class NamedNotebookHandler(AuthenticatedHandler):
284
282
285 @authenticate_unless_readonly
283 @authenticate_unless_readonly
286 def get(self, notebook_id):
284 def get(self, notebook_id):
287 nbm = self.application.notebook_manager
285 nbm = self.application.notebook_manager
288 project = nbm.notebook_dir
286 project = nbm.notebook_dir
289 if not nbm.notebook_exists(notebook_id):
287 if not nbm.notebook_exists(notebook_id):
290 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
288 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
291
289
292 self.render(
290 self.render(
293 'notebook.html', project=project,
291 'notebook.html', project=project,
294 notebook_id=notebook_id,
292 notebook_id=notebook_id,
295 base_project_url=self.application.ipython_app.base_project_url,
293 base_project_url=self.application.ipython_app.base_project_url,
296 base_kernel_url=self.application.ipython_app.base_kernel_url,
294 base_kernel_url=self.application.ipython_app.base_kernel_url,
297 kill_kernel=False,
295 kill_kernel=False,
298 read_only=self.read_only,
296 read_only=self.read_only,
299 logged_in=self.logged_in,
297 logged_in=self.logged_in,
300 login_available=self.login_available,
298 login_available=self.login_available,
301 mathjax_url=self.application.ipython_app.mathjax_url,
299 mathjax_url=self.application.ipython_app.mathjax_url,
302 )
300 )
303
301
304
302
305 class PrintNotebookHandler(AuthenticatedHandler):
303 class PrintNotebookHandler(AuthenticatedHandler):
306
304
307 @authenticate_unless_readonly
305 @authenticate_unless_readonly
308 def get(self, notebook_id):
306 def get(self, notebook_id):
309 nbm = self.application.notebook_manager
307 nbm = self.application.notebook_manager
310 project = nbm.notebook_dir
308 project = nbm.notebook_dir
311 if not nbm.notebook_exists(notebook_id):
309 if not nbm.notebook_exists(notebook_id):
312 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
310 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
313
311
314 self.render(
312 self.render(
315 'printnotebook.html', project=project,
313 'printnotebook.html', project=project,
316 notebook_id=notebook_id,
314 notebook_id=notebook_id,
317 base_project_url=self.application.ipython_app.base_project_url,
315 base_project_url=self.application.ipython_app.base_project_url,
318 base_kernel_url=self.application.ipython_app.base_kernel_url,
316 base_kernel_url=self.application.ipython_app.base_kernel_url,
319 kill_kernel=False,
317 kill_kernel=False,
320 read_only=self.read_only,
318 read_only=self.read_only,
321 logged_in=self.logged_in,
319 logged_in=self.logged_in,
322 login_available=self.login_available,
320 login_available=self.login_available,
323 mathjax_url=self.application.ipython_app.mathjax_url,
321 mathjax_url=self.application.ipython_app.mathjax_url,
324 )
322 )
325
323
326 #-----------------------------------------------------------------------------
324 #-----------------------------------------------------------------------------
327 # Kernel handlers
325 # Kernel handlers
328 #-----------------------------------------------------------------------------
326 #-----------------------------------------------------------------------------
329
327
330
328
331 class MainKernelHandler(AuthenticatedHandler):
329 class MainKernelHandler(AuthenticatedHandler):
332
330
333 @web.authenticated
331 @web.authenticated
334 def get(self):
332 def get(self):
335 km = self.application.kernel_manager
333 km = self.application.kernel_manager
336 self.finish(jsonapi.dumps(km.kernel_ids))
334 self.finish(jsonapi.dumps(km.kernel_ids))
337
335
338 @web.authenticated
336 @web.authenticated
339 def post(self):
337 def post(self):
340 km = self.application.kernel_manager
338 km = self.application.kernel_manager
341 nbm = self.application.notebook_manager
339 nbm = self.application.notebook_manager
342 notebook_id = self.get_argument('notebook', default=None)
340 notebook_id = self.get_argument('notebook', default=None)
343 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
341 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
344 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
342 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
345 self.set_header('Location', '/'+kernel_id)
343 self.set_header('Location', '/'+kernel_id)
346 self.finish(jsonapi.dumps(data))
344 self.finish(jsonapi.dumps(data))
347
345
348
346
349 class KernelHandler(AuthenticatedHandler):
347 class KernelHandler(AuthenticatedHandler):
350
348
351 SUPPORTED_METHODS = ('DELETE')
349 SUPPORTED_METHODS = ('DELETE')
352
350
353 @web.authenticated
351 @web.authenticated
354 def delete(self, kernel_id):
352 def delete(self, kernel_id):
355 km = self.application.kernel_manager
353 km = self.application.kernel_manager
356 km.shutdown_kernel(kernel_id)
354 km.shutdown_kernel(kernel_id)
357 self.set_status(204)
355 self.set_status(204)
358 self.finish()
356 self.finish()
359
357
360
358
361 class KernelActionHandler(AuthenticatedHandler):
359 class KernelActionHandler(AuthenticatedHandler):
362
360
363 @web.authenticated
361 @web.authenticated
364 def post(self, kernel_id, action):
362 def post(self, kernel_id, action):
365 km = self.application.kernel_manager
363 km = self.application.kernel_manager
366 if action == 'interrupt':
364 if action == 'interrupt':
367 km.interrupt_kernel(kernel_id)
365 km.interrupt_kernel(kernel_id)
368 self.set_status(204)
366 self.set_status(204)
369 if action == 'restart':
367 if action == 'restart':
370 new_kernel_id = km.restart_kernel(kernel_id)
368 new_kernel_id = km.restart_kernel(kernel_id)
371 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
369 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
372 self.set_header('Location', '/'+new_kernel_id)
370 self.set_header('Location', '/'+new_kernel_id)
373 self.write(jsonapi.dumps(data))
371 self.write(jsonapi.dumps(data))
374 self.finish()
372 self.finish()
375
373
376
374
377 class ZMQStreamHandler(websocket.WebSocketHandler):
375 class ZMQStreamHandler(websocket.WebSocketHandler):
378
376
379 def _reserialize_reply(self, msg_list):
377 def _reserialize_reply(self, msg_list):
380 """Reserialize a reply message using JSON.
378 """Reserialize a reply message using JSON.
381
379
382 This takes the msg list from the ZMQ socket, unserializes it using
380 This takes the msg list from the ZMQ socket, unserializes it using
383 self.session and then serializes the result using JSON. This method
381 self.session and then serializes the result using JSON. This method
384 should be used by self._on_zmq_reply to build messages that can
382 should be used by self._on_zmq_reply to build messages that can
385 be sent back to the browser.
383 be sent back to the browser.
386 """
384 """
387 idents, msg_list = self.session.feed_identities(msg_list)
385 idents, msg_list = self.session.feed_identities(msg_list)
388 msg = self.session.unserialize(msg_list)
386 msg = self.session.unserialize(msg_list)
389 try:
387 try:
390 msg['header'].pop('date')
388 msg['header'].pop('date')
391 except KeyError:
389 except KeyError:
392 pass
390 pass
393 try:
391 try:
394 msg['parent_header'].pop('date')
392 msg['parent_header'].pop('date')
395 except KeyError:
393 except KeyError:
396 pass
394 pass
397 msg.pop('buffers')
395 msg.pop('buffers')
398 return jsonapi.dumps(msg, default=date_default)
396 return jsonapi.dumps(msg, default=date_default)
399
397
400 def _on_zmq_reply(self, msg_list):
398 def _on_zmq_reply(self, msg_list):
401 try:
399 try:
402 msg = self._reserialize_reply(msg_list)
400 msg = self._reserialize_reply(msg_list)
403 except Exception:
401 except Exception:
404 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
402 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
405 else:
403 else:
406 self.write_message(msg)
404 self.write_message(msg)
407
405
408 def allow_draft76(self):
406 def allow_draft76(self):
409 """Allow draft 76, until browsers such as Safari update to RFC 6455.
407 """Allow draft 76, until browsers such as Safari update to RFC 6455.
410
408
411 This has been disabled by default in tornado in release 2.2.0, and
409 This has been disabled by default in tornado in release 2.2.0, and
412 support will be removed in later versions.
410 support will be removed in later versions.
413 """
411 """
414 return True
412 return True
415
413
416
414
417 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
415 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
418
416
419 def open(self, kernel_id):
417 def open(self, kernel_id):
420 self.kernel_id = kernel_id.decode('ascii')
418 self.kernel_id = kernel_id.decode('ascii')
421 try:
419 try:
422 cfg = self.application.ipython_app.config
420 cfg = self.application.ipython_app.config
423 except AttributeError:
421 except AttributeError:
424 # protect from the case where this is run from something other than
422 # protect from the case where this is run from something other than
425 # the notebook app:
423 # the notebook app:
426 cfg = None
424 cfg = None
427 self.session = Session(config=cfg)
425 self.session = Session(config=cfg)
428 self.save_on_message = self.on_message
426 self.save_on_message = self.on_message
429 self.on_message = self.on_first_message
427 self.on_message = self.on_first_message
430
428
431 def get_current_user(self):
429 def get_current_user(self):
432 user_id = self.get_secure_cookie("username")
430 user_id = self.get_secure_cookie("username")
433 if user_id == '' or (user_id is None and not self.application.password):
431 if user_id == '' or (user_id is None and not self.application.password):
434 user_id = 'anonymous'
432 user_id = 'anonymous'
435 return user_id
433 return user_id
436
434
437 def _inject_cookie_message(self, msg):
435 def _inject_cookie_message(self, msg):
438 """Inject the first message, which is the document cookie,
436 """Inject the first message, which is the document cookie,
439 for authentication."""
437 for authentication."""
440 if isinstance(msg, unicode):
438 if isinstance(msg, unicode):
441 # Cookie can't constructor doesn't accept unicode strings for some reason
439 # Cookie can't constructor doesn't accept unicode strings for some reason
442 msg = msg.encode('utf8', 'replace')
440 msg = msg.encode('utf8', 'replace')
443 try:
441 try:
444 self.request._cookies = Cookie.SimpleCookie(msg)
442 self.request._cookies = Cookie.SimpleCookie(msg)
445 except:
443 except:
446 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
444 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
447
445
448 def on_first_message(self, msg):
446 def on_first_message(self, msg):
449 self._inject_cookie_message(msg)
447 self._inject_cookie_message(msg)
450 if self.get_current_user() is None:
448 if self.get_current_user() is None:
451 logging.warn("Couldn't authenticate WebSocket connection")
449 logging.warn("Couldn't authenticate WebSocket connection")
452 raise web.HTTPError(403)
450 raise web.HTTPError(403)
453 self.on_message = self.save_on_message
451 self.on_message = self.save_on_message
454
452
455
453
456 class IOPubHandler(AuthenticatedZMQStreamHandler):
454 class IOPubHandler(AuthenticatedZMQStreamHandler):
457
455
458 def initialize(self, *args, **kwargs):
456 def initialize(self, *args, **kwargs):
459 self._kernel_alive = True
457 self._kernel_alive = True
460 self._beating = False
458 self._beating = False
461 self.iopub_stream = None
459 self.iopub_stream = None
462 self.hb_stream = None
460 self.hb_stream = None
463
461
464 def on_first_message(self, msg):
462 def on_first_message(self, msg):
465 try:
463 try:
466 super(IOPubHandler, self).on_first_message(msg)
464 super(IOPubHandler, self).on_first_message(msg)
467 except web.HTTPError:
465 except web.HTTPError:
468 self.close()
466 self.close()
469 return
467 return
470 km = self.application.kernel_manager
468 km = self.application.kernel_manager
471 self.time_to_dead = km.time_to_dead
469 self.time_to_dead = km.time_to_dead
472 self.first_beat = km.first_beat
470 self.first_beat = km.first_beat
473 kernel_id = self.kernel_id
471 kernel_id = self.kernel_id
474 try:
472 try:
475 self.iopub_stream = km.create_iopub_stream(kernel_id)
473 self.iopub_stream = km.create_iopub_stream(kernel_id)
476 self.hb_stream = km.create_hb_stream(kernel_id)
474 self.hb_stream = km.create_hb_stream(kernel_id)
477 except web.HTTPError:
475 except web.HTTPError:
478 # WebSockets don't response to traditional error codes so we
476 # WebSockets don't response to traditional error codes so we
479 # close the connection.
477 # close the connection.
480 if not self.stream.closed():
478 if not self.stream.closed():
481 self.stream.close()
479 self.stream.close()
482 self.close()
480 self.close()
483 else:
481 else:
484 self.iopub_stream.on_recv(self._on_zmq_reply)
482 self.iopub_stream.on_recv(self._on_zmq_reply)
485 self.start_hb(self.kernel_died)
483 self.start_hb(self.kernel_died)
486
484
487 def on_message(self, msg):
485 def on_message(self, msg):
488 pass
486 pass
489
487
490 def on_close(self):
488 def on_close(self):
491 # This method can be called twice, once by self.kernel_died and once
489 # This method can be called twice, once by self.kernel_died and once
492 # from the WebSocket close event. If the WebSocket connection is
490 # from the WebSocket close event. If the WebSocket connection is
493 # closed before the ZMQ streams are setup, they could be None.
491 # closed before the ZMQ streams are setup, they could be None.
494 self.stop_hb()
492 self.stop_hb()
495 if self.iopub_stream is not None and not self.iopub_stream.closed():
493 if self.iopub_stream is not None and not self.iopub_stream.closed():
496 self.iopub_stream.on_recv(None)
494 self.iopub_stream.on_recv(None)
497 self.iopub_stream.close()
495 self.iopub_stream.close()
498 if self.hb_stream is not None and not self.hb_stream.closed():
496 if self.hb_stream is not None and not self.hb_stream.closed():
499 self.hb_stream.close()
497 self.hb_stream.close()
500
498
501 def start_hb(self, callback):
499 def start_hb(self, callback):
502 """Start the heartbeating and call the callback if the kernel dies."""
500 """Start the heartbeating and call the callback if the kernel dies."""
503 if not self._beating:
501 if not self._beating:
504 self._kernel_alive = True
502 self._kernel_alive = True
505
503
506 def ping_or_dead():
504 def ping_or_dead():
507 self.hb_stream.flush()
505 self.hb_stream.flush()
508 if self._kernel_alive:
506 if self._kernel_alive:
509 self._kernel_alive = False
507 self._kernel_alive = False
510 self.hb_stream.send(b'ping')
508 self.hb_stream.send(b'ping')
511 # flush stream to force immediate socket send
509 # flush stream to force immediate socket send
512 self.hb_stream.flush()
510 self.hb_stream.flush()
513 else:
511 else:
514 try:
512 try:
515 callback()
513 callback()
516 except:
514 except:
517 pass
515 pass
518 finally:
516 finally:
519 self.stop_hb()
517 self.stop_hb()
520
518
521 def beat_received(msg):
519 def beat_received(msg):
522 self._kernel_alive = True
520 self._kernel_alive = True
523
521
524 self.hb_stream.on_recv(beat_received)
522 self.hb_stream.on_recv(beat_received)
525 loop = ioloop.IOLoop.instance()
523 loop = ioloop.IOLoop.instance()
526 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
524 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
527 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
525 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
528 self._beating= True
526 self._beating= True
529
527
530 def _really_start_hb(self):
528 def _really_start_hb(self):
531 """callback for delayed heartbeat start
529 """callback for delayed heartbeat start
532
530
533 Only start the hb loop if we haven't been closed during the wait.
531 Only start the hb loop if we haven't been closed during the wait.
534 """
532 """
535 if self._beating and not self.hb_stream.closed():
533 if self._beating and not self.hb_stream.closed():
536 self._hb_periodic_callback.start()
534 self._hb_periodic_callback.start()
537
535
538 def stop_hb(self):
536 def stop_hb(self):
539 """Stop the heartbeating and cancel all related callbacks."""
537 """Stop the heartbeating and cancel all related callbacks."""
540 if self._beating:
538 if self._beating:
541 self._beating = False
539 self._beating = False
542 self._hb_periodic_callback.stop()
540 self._hb_periodic_callback.stop()
543 if not self.hb_stream.closed():
541 if not self.hb_stream.closed():
544 self.hb_stream.on_recv(None)
542 self.hb_stream.on_recv(None)
545
543
546 def kernel_died(self):
544 def kernel_died(self):
547 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
545 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
548 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
546 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
549 self.write_message(
547 self.write_message(
550 {'header': {'msg_type': 'status'},
548 {'header': {'msg_type': 'status'},
551 'parent_header': {},
549 'parent_header': {},
552 'content': {'execution_state':'dead'}
550 'content': {'execution_state':'dead'}
553 }
551 }
554 )
552 )
555 self.on_close()
553 self.on_close()
556
554
557
555
558 class ShellHandler(AuthenticatedZMQStreamHandler):
556 class ShellHandler(AuthenticatedZMQStreamHandler):
559
557
560 def initialize(self, *args, **kwargs):
558 def initialize(self, *args, **kwargs):
561 self.shell_stream = None
559 self.shell_stream = None
562
560
563 def on_first_message(self, msg):
561 def on_first_message(self, msg):
564 try:
562 try:
565 super(ShellHandler, self).on_first_message(msg)
563 super(ShellHandler, self).on_first_message(msg)
566 except web.HTTPError:
564 except web.HTTPError:
567 self.close()
565 self.close()
568 return
566 return
569 km = self.application.kernel_manager
567 km = self.application.kernel_manager
570 self.max_msg_size = km.max_msg_size
568 self.max_msg_size = km.max_msg_size
571 kernel_id = self.kernel_id
569 kernel_id = self.kernel_id
572 try:
570 try:
573 self.shell_stream = km.create_shell_stream(kernel_id)
571 self.shell_stream = km.create_shell_stream(kernel_id)
574 except web.HTTPError:
572 except web.HTTPError:
575 # WebSockets don't response to traditional error codes so we
573 # WebSockets don't response to traditional error codes so we
576 # close the connection.
574 # close the connection.
577 if not self.stream.closed():
575 if not self.stream.closed():
578 self.stream.close()
576 self.stream.close()
579 self.close()
577 self.close()
580 else:
578 else:
581 self.shell_stream.on_recv(self._on_zmq_reply)
579 self.shell_stream.on_recv(self._on_zmq_reply)
582
580
583 def on_message(self, msg):
581 def on_message(self, msg):
584 if len(msg) < self.max_msg_size:
582 if len(msg) < self.max_msg_size:
585 msg = jsonapi.loads(msg)
583 msg = jsonapi.loads(msg)
586 self.session.send(self.shell_stream, msg)
584 self.session.send(self.shell_stream, msg)
587
585
588 def on_close(self):
586 def on_close(self):
589 # Make sure the stream exists and is not already closed.
587 # Make sure the stream exists and is not already closed.
590 if self.shell_stream is not None and not self.shell_stream.closed():
588 if self.shell_stream is not None and not self.shell_stream.closed():
591 self.shell_stream.close()
589 self.shell_stream.close()
592
590
593
591
594 #-----------------------------------------------------------------------------
592 #-----------------------------------------------------------------------------
595 # Notebook web service handlers
593 # Notebook web service handlers
596 #-----------------------------------------------------------------------------
594 #-----------------------------------------------------------------------------
597
595
598 class NotebookRootHandler(AuthenticatedHandler):
596 class NotebookRootHandler(AuthenticatedHandler):
599
597
600 @authenticate_unless_readonly
598 @authenticate_unless_readonly
601 def get(self):
599 def get(self):
602 nbm = self.application.notebook_manager
600 nbm = self.application.notebook_manager
603 km = self.application.kernel_manager
601 km = self.application.kernel_manager
604 files = nbm.list_notebooks()
602 files = nbm.list_notebooks()
605 for f in files :
603 for f in files :
606 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
604 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
607 self.finish(jsonapi.dumps(files))
605 self.finish(jsonapi.dumps(files))
608
606
609 @web.authenticated
607 @web.authenticated
610 def post(self):
608 def post(self):
611 nbm = self.application.notebook_manager
609 nbm = self.application.notebook_manager
612 body = self.request.body.strip()
610 body = self.request.body.strip()
613 format = self.get_argument('format', default='json')
611 format = self.get_argument('format', default='json')
614 name = self.get_argument('name', default=None)
612 name = self.get_argument('name', default=None)
615 if body:
613 if body:
616 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
614 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
617 else:
615 else:
618 notebook_id = nbm.new_notebook()
616 notebook_id = nbm.new_notebook()
619 self.set_header('Location', '/'+notebook_id)
617 self.set_header('Location', '/'+notebook_id)
620 self.finish(jsonapi.dumps(notebook_id))
618 self.finish(jsonapi.dumps(notebook_id))
621
619
622
620
623 class NotebookHandler(AuthenticatedHandler):
621 class NotebookHandler(AuthenticatedHandler):
624
622
625 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
623 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
626
624
627 @authenticate_unless_readonly
625 @authenticate_unless_readonly
628 def get(self, notebook_id):
626 def get(self, notebook_id):
629 nbm = self.application.notebook_manager
627 nbm = self.application.notebook_manager
630 format = self.get_argument('format', default='json')
628 format = self.get_argument('format', default='json')
631 last_mod, name, data = nbm.get_notebook(notebook_id, format)
629 last_mod, name, data = nbm.get_notebook(notebook_id, format)
632
630
633 if format == u'json':
631 if format == u'json':
634 self.set_header('Content-Type', 'application/json')
632 self.set_header('Content-Type', 'application/json')
635 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
633 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
636 elif format == u'py':
634 elif format == u'py':
637 self.set_header('Content-Type', 'application/x-python')
635 self.set_header('Content-Type', 'application/x-python')
638 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
636 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
639 self.set_header('Last-Modified', last_mod)
637 self.set_header('Last-Modified', last_mod)
640 self.finish(data)
638 self.finish(data)
641
639
642 @web.authenticated
640 @web.authenticated
643 def put(self, notebook_id):
641 def put(self, notebook_id):
644 nbm = self.application.notebook_manager
642 nbm = self.application.notebook_manager
645 format = self.get_argument('format', default='json')
643 format = self.get_argument('format', default='json')
646 name = self.get_argument('name', default=None)
644 name = self.get_argument('name', default=None)
647 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
645 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
648 self.set_status(204)
646 self.set_status(204)
649 self.finish()
647 self.finish()
650
648
651 @web.authenticated
649 @web.authenticated
652 def delete(self, notebook_id):
650 def delete(self, notebook_id):
653 nbm = self.application.notebook_manager
651 nbm = self.application.notebook_manager
654 nbm.delete_notebook(notebook_id)
652 nbm.delete_notebook(notebook_id)
655 self.set_status(204)
653 self.set_status(204)
656 self.finish()
654 self.finish()
657
655
658
656
659 class NotebookCopyHandler(AuthenticatedHandler):
657 class NotebookCopyHandler(AuthenticatedHandler):
660
658
661 @web.authenticated
659 @web.authenticated
662 def get(self, notebook_id):
660 def get(self, notebook_id):
663 nbm = self.application.notebook_manager
661 nbm = self.application.notebook_manager
664 project = nbm.notebook_dir
662 project = nbm.notebook_dir
665 notebook_id = nbm.copy_notebook(notebook_id)
663 notebook_id = nbm.copy_notebook(notebook_id)
666 self.render(
664 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
667 'notebook.html', project=project,
668 notebook_id=notebook_id,
669 base_project_url=self.application.ipython_app.base_project_url,
670 base_kernel_url=self.application.ipython_app.base_kernel_url,
671 kill_kernel=False,
672 read_only=False,
673 logged_in=self.logged_in,
674 login_available=self.login_available,
675 mathjax_url=self.application.ipython_app.mathjax_url,
676 )
677
665
678
666
679 #-----------------------------------------------------------------------------
667 #-----------------------------------------------------------------------------
680 # Cluster handlers
668 # Cluster handlers
681 #-----------------------------------------------------------------------------
669 #-----------------------------------------------------------------------------
682
670
683
671
684 class MainClusterHandler(AuthenticatedHandler):
672 class MainClusterHandler(AuthenticatedHandler):
685
673
686 @web.authenticated
674 @web.authenticated
687 def get(self):
675 def get(self):
688 cm = self.application.cluster_manager
676 cm = self.application.cluster_manager
689 self.finish(jsonapi.dumps(cm.list_profiles()))
677 self.finish(jsonapi.dumps(cm.list_profiles()))
690
678
691
679
692 class ClusterProfileHandler(AuthenticatedHandler):
680 class ClusterProfileHandler(AuthenticatedHandler):
693
681
694 @web.authenticated
682 @web.authenticated
695 def get(self, profile):
683 def get(self, profile):
696 cm = self.application.cluster_manager
684 cm = self.application.cluster_manager
697 self.finish(jsonapi.dumps(cm.profile_info(profile)))
685 self.finish(jsonapi.dumps(cm.profile_info(profile)))
698
686
699
687
700 class ClusterActionHandler(AuthenticatedHandler):
688 class ClusterActionHandler(AuthenticatedHandler):
701
689
702 @web.authenticated
690 @web.authenticated
703 def post(self, profile, action):
691 def post(self, profile, action):
704 cm = self.application.cluster_manager
692 cm = self.application.cluster_manager
705 if action == 'start':
693 if action == 'start':
706 n = self.get_argument('n',default=None)
694 n = self.get_argument('n',default=None)
707 if n is None:
695 if n is None:
708 data = cm.start_cluster(profile)
696 data = cm.start_cluster(profile)
709 else:
697 else:
710 data = cm.start_cluster(profile,int(n))
698 data = cm.start_cluster(profile,int(n))
711 if action == 'stop':
699 if action == 'stop':
712 data = cm.stop_cluster(profile)
700 data = cm.stop_cluster(profile)
713 self.finish(jsonapi.dumps(data))
701 self.finish(jsonapi.dumps(data))
714
702
715
703
716 #-----------------------------------------------------------------------------
704 #-----------------------------------------------------------------------------
717 # RST web service handlers
705 # RST web service handlers
718 #-----------------------------------------------------------------------------
706 #-----------------------------------------------------------------------------
719
707
720
708
721 class RSTHandler(AuthenticatedHandler):
709 class RSTHandler(AuthenticatedHandler):
722
710
723 @web.authenticated
711 @web.authenticated
724 def post(self):
712 def post(self):
725 if publish_string is None:
713 if publish_string is None:
726 raise web.HTTPError(503, u'docutils not available')
714 raise web.HTTPError(503, u'docutils not available')
727 body = self.request.body.strip()
715 body = self.request.body.strip()
728 source = body
716 source = body
729 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
717 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
730 defaults = {'file_insertion_enabled': 0,
718 defaults = {'file_insertion_enabled': 0,
731 'raw_enabled': 0,
719 'raw_enabled': 0,
732 '_disable_config': 1,
720 '_disable_config': 1,
733 'stylesheet_path': 0
721 'stylesheet_path': 0
734 # 'template': template_path
722 # 'template': template_path
735 }
723 }
736 try:
724 try:
737 html = publish_string(source, writer_name='html',
725 html = publish_string(source, writer_name='html',
738 settings_overrides=defaults
726 settings_overrides=defaults
739 )
727 )
740 except:
728 except:
741 raise web.HTTPError(400, u'Invalid RST')
729 raise web.HTTPError(400, u'Invalid RST')
742 print html
730 print html
743 self.set_header('Content-Type', 'text/html')
731 self.set_header('Content-Type', 'text/html')
744 self.finish(html)
732 self.finish(html)
745
733
746 # to minimize subclass changes:
734 # to minimize subclass changes:
747 HTTPError = web.HTTPError
735 HTTPError = web.HTTPError
748
736
749 class FileFindHandler(web.StaticFileHandler):
737 class FileFindHandler(web.StaticFileHandler):
750 """subclass of StaticFileHandler for serving files from a search path"""
738 """subclass of StaticFileHandler for serving files from a search path"""
751
739
752 _static_paths = {}
740 _static_paths = {}
753 # _lock is needed for tornado < 2.2.0 compat
741 # _lock is needed for tornado < 2.2.0 compat
754 _lock = threading.Lock() # protects _static_hashes
742 _lock = threading.Lock() # protects _static_hashes
755
743
756 def initialize(self, path, default_filename=None):
744 def initialize(self, path, default_filename=None):
757 if isinstance(path, basestring):
745 if isinstance(path, basestring):
758 path = [path]
746 path = [path]
759 self.roots = tuple(
747 self.roots = tuple(
760 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
748 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
761 )
749 )
762 self.default_filename = default_filename
750 self.default_filename = default_filename
763
751
764 @classmethod
752 @classmethod
765 def locate_file(cls, path, roots):
753 def locate_file(cls, path, roots):
766 """locate a file to serve on our static file search path"""
754 """locate a file to serve on our static file search path"""
767 with cls._lock:
755 with cls._lock:
768 if path in cls._static_paths:
756 if path in cls._static_paths:
769 return cls._static_paths[path]
757 return cls._static_paths[path]
770 try:
758 try:
771 abspath = os.path.abspath(filefind(path, roots))
759 abspath = os.path.abspath(filefind(path, roots))
772 except IOError:
760 except IOError:
773 # empty string should always give exists=False
761 # empty string should always give exists=False
774 return ''
762 return ''
775
763
776 # os.path.abspath strips a trailing /
764 # os.path.abspath strips a trailing /
777 # it needs to be temporarily added back for requests to root/
765 # it needs to be temporarily added back for requests to root/
778 if not (abspath + os.path.sep).startswith(roots):
766 if not (abspath + os.path.sep).startswith(roots):
779 raise HTTPError(403, "%s is not in root static directory", path)
767 raise HTTPError(403, "%s is not in root static directory", path)
780
768
781 cls._static_paths[path] = abspath
769 cls._static_paths[path] = abspath
782 return abspath
770 return abspath
783
771
784 def get(self, path, include_body=True):
772 def get(self, path, include_body=True):
785 path = self.parse_url_path(path)
773 path = self.parse_url_path(path)
786
774
787 # begin subclass override
775 # begin subclass override
788 abspath = self.locate_file(path, self.roots)
776 abspath = self.locate_file(path, self.roots)
789 # end subclass override
777 # end subclass override
790
778
791 if os.path.isdir(abspath) and self.default_filename is not None:
779 if os.path.isdir(abspath) and self.default_filename is not None:
792 # need to look at the request.path here for when path is empty
780 # need to look at the request.path here for when path is empty
793 # but there is some prefix to the path that was already
781 # but there is some prefix to the path that was already
794 # trimmed by the routing
782 # trimmed by the routing
795 if not self.request.path.endswith("/"):
783 if not self.request.path.endswith("/"):
796 self.redirect(self.request.path + "/")
784 self.redirect(self.request.path + "/")
797 return
785 return
798 abspath = os.path.join(abspath, self.default_filename)
786 abspath = os.path.join(abspath, self.default_filename)
799 if not os.path.exists(abspath):
787 if not os.path.exists(abspath):
800 raise HTTPError(404)
788 raise HTTPError(404)
801 if not os.path.isfile(abspath):
789 if not os.path.isfile(abspath):
802 raise HTTPError(403, "%s is not a file", path)
790 raise HTTPError(403, "%s is not a file", path)
803
791
804 stat_result = os.stat(abspath)
792 stat_result = os.stat(abspath)
805 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
793 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
806
794
807 self.set_header("Last-Modified", modified)
795 self.set_header("Last-Modified", modified)
808
796
809 mime_type, encoding = mimetypes.guess_type(abspath)
797 mime_type, encoding = mimetypes.guess_type(abspath)
810 if mime_type:
798 if mime_type:
811 self.set_header("Content-Type", mime_type)
799 self.set_header("Content-Type", mime_type)
812
800
813 cache_time = self.get_cache_time(path, modified, mime_type)
801 cache_time = self.get_cache_time(path, modified, mime_type)
814
802
815 if cache_time > 0:
803 if cache_time > 0:
816 self.set_header("Expires", datetime.datetime.utcnow() + \
804 self.set_header("Expires", datetime.datetime.utcnow() + \
817 datetime.timedelta(seconds=cache_time))
805 datetime.timedelta(seconds=cache_time))
818 self.set_header("Cache-Control", "max-age=" + str(cache_time))
806 self.set_header("Cache-Control", "max-age=" + str(cache_time))
819 else:
807 else:
820 self.set_header("Cache-Control", "public")
808 self.set_header("Cache-Control", "public")
821
809
822 self.set_extra_headers(path)
810 self.set_extra_headers(path)
823
811
824 # Check the If-Modified-Since, and don't send the result if the
812 # Check the If-Modified-Since, and don't send the result if the
825 # content has not been modified
813 # content has not been modified
826 ims_value = self.request.headers.get("If-Modified-Since")
814 ims_value = self.request.headers.get("If-Modified-Since")
827 if ims_value is not None:
815 if ims_value is not None:
828 date_tuple = email.utils.parsedate(ims_value)
816 date_tuple = email.utils.parsedate(ims_value)
829 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
817 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
830 if if_since >= modified:
818 if if_since >= modified:
831 self.set_status(304)
819 self.set_status(304)
832 return
820 return
833
821
834 with open(abspath, "rb") as file:
822 with open(abspath, "rb") as file:
835 data = file.read()
823 data = file.read()
836 hasher = hashlib.sha1()
824 hasher = hashlib.sha1()
837 hasher.update(data)
825 hasher.update(data)
838 self.set_header("Etag", '"%s"' % hasher.hexdigest())
826 self.set_header("Etag", '"%s"' % hasher.hexdigest())
839 if include_body:
827 if include_body:
840 self.write(data)
828 self.write(data)
841 else:
829 else:
842 assert self.request.method == "HEAD"
830 assert self.request.method == "HEAD"
843 self.set_header("Content-Length", len(data))
831 self.set_header("Content-Length", len(data))
844
832
845 @classmethod
833 @classmethod
846 def get_version(cls, settings, path):
834 def get_version(cls, settings, path):
847 """Generate the version string to be used in static URLs.
835 """Generate the version string to be used in static URLs.
848
836
849 This method may be overridden in subclasses (but note that it
837 This method may be overridden in subclasses (but note that it
850 is a class method rather than a static method). The default
838 is a class method rather than a static method). The default
851 implementation uses a hash of the file's contents.
839 implementation uses a hash of the file's contents.
852
840
853 ``settings`` is the `Application.settings` dictionary and ``path``
841 ``settings`` is the `Application.settings` dictionary and ``path``
854 is the relative location of the requested asset on the filesystem.
842 is the relative location of the requested asset on the filesystem.
855 The returned value should be a string, or ``None`` if no version
843 The returned value should be a string, or ``None`` if no version
856 could be determined.
844 could be determined.
857 """
845 """
858 # begin subclass override:
846 # begin subclass override:
859 static_paths = settings['static_path']
847 static_paths = settings['static_path']
860 if isinstance(static_paths, basestring):
848 if isinstance(static_paths, basestring):
861 static_paths = [static_paths]
849 static_paths = [static_paths]
862 roots = tuple(
850 roots = tuple(
863 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
851 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
864 )
852 )
865
853
866 try:
854 try:
867 abs_path = filefind(path, roots)
855 abs_path = filefind(path, roots)
868 except IOError:
856 except IOError:
869 logging.error("Could not find static file %r", path)
857 logging.error("Could not find static file %r", path)
870 return None
858 return None
871
859
872 # end subclass override
860 # end subclass override
873
861
874 with cls._lock:
862 with cls._lock:
875 hashes = cls._static_hashes
863 hashes = cls._static_hashes
876 if abs_path not in hashes:
864 if abs_path not in hashes:
877 try:
865 try:
878 f = open(abs_path, "rb")
866 f = open(abs_path, "rb")
879 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
867 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
880 f.close()
868 f.close()
881 except Exception:
869 except Exception:
882 logging.error("Could not open static file %r", path)
870 logging.error("Could not open static file %r", path)
883 hashes[abs_path] = None
871 hashes[abs_path] = None
884 hsh = hashes.get(abs_path)
872 hsh = hashes.get(abs_path)
885 if hsh:
873 if hsh:
886 return hsh[:5]
874 return hsh[:5]
887 return None
875 return None
888
876
889
877
890 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
878 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
891 # but needed for tornado < 2.2.0 compat
879 # but needed for tornado < 2.2.0 compat
892 @classmethod
880 @classmethod
893 def make_static_url(cls, settings, path):
881 def make_static_url(cls, settings, path):
894 """Constructs a versioned url for the given path.
882 """Constructs a versioned url for the given path.
895
883
896 This method may be overridden in subclasses (but note that it is
884 This method may be overridden in subclasses (but note that it is
897 a class method rather than an instance method).
885 a class method rather than an instance method).
898
886
899 ``settings`` is the `Application.settings` dictionary. ``path``
887 ``settings`` is the `Application.settings` dictionary. ``path``
900 is the static path being requested. The url returned should be
888 is the static path being requested. The url returned should be
901 relative to the current host.
889 relative to the current host.
902 """
890 """
903 static_url_prefix = settings.get('static_url_prefix', '/static/')
891 static_url_prefix = settings.get('static_url_prefix', '/static/')
904 version_hash = cls.get_version(settings, path)
892 version_hash = cls.get_version(settings, path)
905 if version_hash:
893 if version_hash:
906 return static_url_prefix + path + "?v=" + version_hash
894 return static_url_prefix + path + "?v=" + version_hash
907 return static_url_prefix + path
895 return static_url_prefix + path
908
896
909 def parse_url_path(self, url_path):
897 def parse_url_path(self, url_path):
910 """Converts a static URL path into a filesystem path.
898 """Converts a static URL path into a filesystem path.
911
899
912 ``url_path`` is the path component of the URL with
900 ``url_path`` is the path component of the URL with
913 ``static_url_prefix`` removed. The return value should be
901 ``static_url_prefix`` removed. The return value should be
914 filesystem path relative to ``static_path``.
902 filesystem path relative to ``static_path``.
915 """
903 """
916 if os.path.sep != "/":
904 if os.path.sep != "/":
917 url_path = url_path.replace("/", os.path.sep)
905 url_path = url_path.replace("/", os.path.sep)
918 return url_path
906 return url_path
919
907
920
908
@@ -1,68 +1,67 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // On document ready
9 // On document ready
10 //============================================================================
10 //============================================================================
11
11
12
12
13 $(document).ready(function () {
13 $(document).ready(function () {
14
14
15 IPython.init_mathjax();
15 IPython.init_mathjax();
16
16
17 IPython.read_only = $('body').data('readOnly') === 'True';
17 IPython.read_only = $('body').data('readOnly') === 'True';
18 $('div#main_app').addClass('border-box-sizing ui-widget');
18 $('div#main_app').addClass('border-box-sizing ui-widget');
19 $('div#notebook_panel').addClass('border-box-sizing ui-widget');
19 $('div#notebook_panel').addClass('border-box-sizing ui-widget');
20 // The header's bottom border is provided by the menu bar so we remove it.
20 // The header's bottom border is provided by the menu bar so we remove it.
21 $('div#header').css('border-bottom-style','none');
21 $('div#header').css('border-bottom-style','none');
22
22
23 IPython.page = new IPython.Page();
23 IPython.page = new IPython.Page();
24 IPython.markdown_converter = new Markdown.Converter();
24 IPython.markdown_converter = new Markdown.Converter();
25 IPython.layout_manager = new IPython.LayoutManager();
25 IPython.layout_manager = new IPython.LayoutManager();
26 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
26 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
27 IPython.quick_help = new IPython.QuickHelp('span#quick_help_area');
27 IPython.quick_help = new IPython.QuickHelp('span#quick_help_area');
28 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
28 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
29 IPython.notebook = new IPython.Notebook('div#notebook');
29 IPython.notebook = new IPython.Notebook('div#notebook');
30 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
30 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
31 IPython.menubar = new IPython.MenuBar('#menubar')
31 IPython.menubar = new IPython.MenuBar('#menubar')
32 IPython.toolbar = new IPython.ToolBar('#toolbar')
32 IPython.toolbar = new IPython.ToolBar('#toolbar')
33 IPython.tooltip = new IPython.Tooltip()
33 IPython.tooltip = new IPython.Tooltip()
34 IPython.notification_widget = new IPython.NotificationWidget('#notification')
34 IPython.notification_widget = new IPython.NotificationWidget('#notification')
35
35
36 IPython.layout_manager.do_resize();
36 IPython.layout_manager.do_resize();
37
37
38 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
38 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
39 '<span id="test2" style="font-weight: bold;">x</span>'+
39 '<span id="test2" style="font-weight: bold;">x</span>'+
40 '<span id="test3" style="font-style: italic;">x</span></pre></div>')
40 '<span id="test3" style="font-style: italic;">x</span></pre></div>')
41 var nh = $('#test1').innerHeight();
41 var nh = $('#test1').innerHeight();
42 var bh = $('#test2').innerHeight();
42 var bh = $('#test2').innerHeight();
43 var ih = $('#test3').innerHeight();
43 var ih = $('#test3').innerHeight();
44 if(nh != bh || nh != ih) {
44 if(nh != bh || nh != ih) {
45 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
45 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
46 }
46 }
47 $('#fonttest').remove();
47 $('#fonttest').remove();
48
48
49 if(IPython.read_only){
49 if(IPython.read_only){
50 // hide various elements from read-only view
50 // hide various elements from read-only view
51 $('div#pager').remove();
51 $('div#pager').remove();
52 $('div#pager_splitter').remove();
52 $('div#pager_splitter').remove();
53
53
54 // set the notebook name field as not modifiable
54 // set the notebook name field as not modifiable
55 $('#notebook_name').attr('disabled','disabled')
55 $('#notebook_name').attr('disabled','disabled')
56 }
56 }
57
57
58 IPython.page.show();
58 IPython.page.show();
59
59
60 IPython.layout_manager.do_resize();
60 IPython.layout_manager.do_resize();
61 $([IPython.events]).on('notebook_loaded.Notebook', function () {
61 $([IPython.events]).on('notebook_loaded.Notebook', function () {
62 IPython.layout_manager.do_resize();
62 IPython.layout_manager.do_resize();
63 IPython.save_widget.update_url();
64 })
63 })
65 IPython.notebook.load_notebook($('body').data('notebookId'));
64 IPython.notebook.load_notebook($('body').data('notebookId'));
66
65
67 });
66 });
68
67
@@ -1,148 +1,139 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // SaveWidget
9 // SaveWidget
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
13
14 var utils = IPython.utils;
14 var utils = IPython.utils;
15
15
16 var SaveWidget = function (selector) {
16 var SaveWidget = function (selector) {
17 this.selector = selector;
17 this.selector = selector;
18 if (this.selector !== undefined) {
18 if (this.selector !== undefined) {
19 this.element = $(selector);
19 this.element = $(selector);
20 this.style();
20 this.style();
21 this.bind_events();
21 this.bind_events();
22 }
22 }
23 };
23 };
24
24
25
25
26 SaveWidget.prototype.style = function () {
26 SaveWidget.prototype.style = function () {
27 this.element.find('span#save_widget').addClass('ui-widget');
27 this.element.find('span#save_widget').addClass('ui-widget');
28 this.element.find('span#notebook_name').addClass('ui-widget ui-widget-content');
28 this.element.find('span#notebook_name').addClass('ui-widget ui-widget-content');
29 this.element.find('span#save_status').addClass('ui-widget ui-widget-content')
29 this.element.find('span#save_status').addClass('ui-widget ui-widget-content')
30 .css({border: 'none', 'margin-left': '20px'});
30 .css({border: 'none', 'margin-left': '20px'});
31 };
31 };
32
32
33
33
34 SaveWidget.prototype.bind_events = function () {
34 SaveWidget.prototype.bind_events = function () {
35 var that = this;
35 var that = this;
36 this.element.find('span#notebook_name').click(function () {
36 this.element.find('span#notebook_name').click(function () {
37 that.rename_notebook();
37 that.rename_notebook();
38 });
38 });
39 this.element.find('span#notebook_name').hover(function () {
39 this.element.find('span#notebook_name').hover(function () {
40 $(this).addClass("ui-state-hover");
40 $(this).addClass("ui-state-hover");
41 }, function () {
41 }, function () {
42 $(this).removeClass("ui-state-hover");
42 $(this).removeClass("ui-state-hover");
43 });
43 });
44 $([IPython.events]).on('notebook_loaded.Notebook', function () {
44 $([IPython.events]).on('notebook_loaded.Notebook', function () {
45 that.set_last_saved();
45 that.set_last_saved();
46 that.update_notebook_name();
46 that.update_notebook_name();
47 that.update_document_title();
47 that.update_document_title();
48 });
48 });
49 $([IPython.events]).on('notebook_saved.Notebook', function () {
49 $([IPython.events]).on('notebook_saved.Notebook', function () {
50 that.set_last_saved();
50 that.set_last_saved();
51 that.update_notebook_name();
51 that.update_notebook_name();
52 that.update_document_title();
52 that.update_document_title();
53 });
53 });
54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
55 that.set_save_status('Last Save Failed!');
55 that.set_save_status('Last Save Failed!');
56 });
56 });
57 };
57 };
58
58
59
59
60 SaveWidget.prototype.rename_notebook = function () {
60 SaveWidget.prototype.rename_notebook = function () {
61 var that = this;
61 var that = this;
62 var dialog = $('<div/>');
62 var dialog = $('<div/>');
63 dialog.append(
63 dialog.append(
64 $('<h3/>').html('Enter a new notebook name:')
64 $('<h3/>').html('Enter a new notebook name:')
65 .css({'margin-bottom': '10px'})
65 .css({'margin-bottom': '10px'})
66 );
66 );
67 dialog.append(
67 dialog.append(
68 $('<input/>').attr('type','text').attr('size','25')
68 $('<input/>').attr('type','text').attr('size','25')
69 .addClass('ui-widget ui-widget-content')
69 .addClass('ui-widget ui-widget-content')
70 .attr('value',IPython.notebook.get_notebook_name())
70 .attr('value',IPython.notebook.get_notebook_name())
71 );
71 );
72 // $(document).append(dialog);
72 // $(document).append(dialog);
73 dialog.dialog({
73 dialog.dialog({
74 resizable: false,
74 resizable: false,
75 modal: true,
75 modal: true,
76 title: "Rename Notebook",
76 title: "Rename Notebook",
77 closeText: "",
77 closeText: "",
78 close: function(event, ui) {$(this).dialog('destroy').remove();},
78 close: function(event, ui) {$(this).dialog('destroy').remove();},
79 buttons : {
79 buttons : {
80 "OK": function () {
80 "OK": function () {
81 var new_name = $(this).find('input').attr('value');
81 var new_name = $(this).find('input').attr('value');
82 if (!IPython.notebook.test_notebook_name(new_name)) {
82 if (!IPython.notebook.test_notebook_name(new_name)) {
83 $(this).find('h3').html(
83 $(this).find('h3').html(
84 "Invalid notebook name. Notebook names must "+
84 "Invalid notebook name. Notebook names must "+
85 "have 1 or more characters and can contain any characters " +
85 "have 1 or more characters and can contain any characters " +
86 "except :/\\. Please enter a new notebook name:"
86 "except :/\\. Please enter a new notebook name:"
87 );
87 );
88 } else {
88 } else {
89 IPython.notebook.set_notebook_name(new_name);
89 IPython.notebook.set_notebook_name(new_name);
90 IPython.notebook.save_notebook();
90 IPython.notebook.save_notebook();
91 $(this).dialog('close');
91 $(this).dialog('close');
92 }
92 }
93 },
93 },
94 "Cancel": function () {
94 "Cancel": function () {
95 $(this).dialog('close');
95 $(this).dialog('close');
96 }
96 }
97 },
97 },
98 open : function (event, ui) {
98 open : function (event, ui) {
99 var that = $(this);
99 var that = $(this);
100 // Upon ENTER, click the OK button.
100 // Upon ENTER, click the OK button.
101 that.find('input[type="text"]').keydown(function (event, ui) {
101 that.find('input[type="text"]').keydown(function (event, ui) {
102 if (event.which === utils.keycodes.ENTER) {
102 if (event.which === utils.keycodes.ENTER) {
103 that.parent().find('button').first().click();
103 that.parent().find('button').first().click();
104 }
104 }
105 });
105 });
106 }
106 }
107 });
107 });
108 }
108 }
109
109
110
110
111 SaveWidget.prototype.update_notebook_name = function () {
111 SaveWidget.prototype.update_notebook_name = function () {
112 var nbname = IPython.notebook.get_notebook_name();
112 var nbname = IPython.notebook.get_notebook_name();
113 this.element.find('span#notebook_name').html(nbname);
113 this.element.find('span#notebook_name').html(nbname);
114 };
114 };
115
115
116
116
117 SaveWidget.prototype.update_document_title = function () {
117 SaveWidget.prototype.update_document_title = function () {
118 var nbname = IPython.notebook.get_notebook_name();
118 var nbname = IPython.notebook.get_notebook_name();
119 document.title = nbname;
119 document.title = nbname;
120 };
120 };
121
121
122
122
123 SaveWidget.prototype.update_url = function () {
124 var notebook_id = IPython.notebook.get_notebook_id();
125 if (notebook_id !== null) {
126 var new_url = $('body').data('baseProjectUrl') + notebook_id;
127 window.history.replaceState({}, '', new_url);
128 };
129 };
130
131
132 SaveWidget.prototype.set_save_status = function (msg) {
123 SaveWidget.prototype.set_save_status = function (msg) {
133 this.element.find('span#save_status').html(msg);
124 this.element.find('span#save_status').html(msg);
134 }
125 }
135
126
136
127
137 SaveWidget.prototype.set_last_saved = function () {
128 SaveWidget.prototype.set_last_saved = function () {
138 var d = new Date();
129 var d = new Date();
139 this.set_save_status('Last saved: '+d.format('mmm dd h:MM TT'));
130 this.set_save_status('Last saved: '+d.format('mmm dd h:MM TT'));
140 };
131 };
141
132
142
133
143 IPython.SaveWidget = SaveWidget;
134 IPython.SaveWidget = SaveWidget;
144
135
145 return IPython;
136 return IPython;
146
137
147 }(IPython));
138 }(IPython));
148
139
General Comments 0
You need to be logged in to leave comments. Login now