diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py
index 1ce17b5..97b2937 100644
--- a/IPython/frontend/html/notebook/handlers.py
+++ b/IPython/frontend/html/notebook/handlers.py
@@ -26,6 +26,7 @@ from tornado import websocket
from zmq.eventloop import ioloop
from zmq.utils import jsonapi
+from IPython.external.decorator import decorator
from IPython.zmq.session import Session
try:
@@ -34,6 +35,16 @@ except ImportError:
publish_string = None
+#-----------------------------------------------------------------------------
+# Decorator for disabling read-only handlers
+#-----------------------------------------------------------------------------
+
+@decorator
+def not_if_readonly(f, self, *args, **kwargs):
+ if self.application.ipython_app.read_only:
+ raise web.HTTPError(403, "Notebook server is read-only")
+ else:
+ return f(self, *args, **kwargs)
#-----------------------------------------------------------------------------
# Top-level handlers
@@ -82,6 +93,7 @@ class LoginHandler(AuthenticatedHandler):
class NewHandler(AuthenticatedHandler):
+ @not_if_readonly
@web.authenticated
def get(self):
nbm = self.application.notebook_manager
@@ -118,11 +130,13 @@ class NamedNotebookHandler(AuthenticatedHandler):
class MainKernelHandler(AuthenticatedHandler):
+ @not_if_readonly
@web.authenticated
def get(self):
km = self.application.kernel_manager
self.finish(jsonapi.dumps(km.kernel_ids))
+ @not_if_readonly
@web.authenticated
def post(self):
km = self.application.kernel_manager
@@ -138,6 +152,7 @@ class KernelHandler(AuthenticatedHandler):
SUPPORTED_METHODS = ('DELETE')
+ @not_if_readonly
@web.authenticated
def delete(self, kernel_id):
km = self.application.kernel_manager
@@ -148,6 +163,7 @@ class KernelHandler(AuthenticatedHandler):
class KernelActionHandler(AuthenticatedHandler):
+ @not_if_readonly
@web.authenticated
def post(self, kernel_id, action):
km = self.application.kernel_manager
@@ -226,6 +242,7 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
except:
logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
+ @not_if_readonly
def on_first_message(self, msg):
self._inject_cookie_message(msg)
if self.get_current_user() is None:
@@ -369,6 +386,7 @@ class NotebookRootHandler(AuthenticatedHandler):
files = nbm.list_notebooks()
self.finish(jsonapi.dumps(files))
+ @not_if_readonly
@web.authenticated
def post(self):
nbm = self.application.notebook_manager
@@ -401,6 +419,7 @@ class NotebookHandler(AuthenticatedHandler):
self.set_header('Last-Modified', last_mod)
self.finish(data)
+ @not_if_readonly
@web.authenticated
def put(self, notebook_id):
nbm = self.application.notebook_manager
@@ -410,6 +429,7 @@ class NotebookHandler(AuthenticatedHandler):
self.set_status(204)
self.finish()
+ @not_if_readonly
@web.authenticated
def delete(self, notebook_id):
nbm = self.application.notebook_manager
diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py
index d037f95..27db5ad 100644
--- a/IPython/frontend/html/notebook/notebookapp.py
+++ b/IPython/frontend/html/notebook/notebookapp.py
@@ -116,6 +116,10 @@ flags['no-browser']=(
{'NotebookApp' : {'open_browser' : False}},
"Don't open the notebook in a browser after startup."
)
+flags['read-only'] = (
+ {'NotebookApp' : {'read_only' : True}},
+ "Launch the Notebook server in read-only mode, not allowing execution or editing"
+)
# the flags that are specific to the frontend
# these must be scrubbed before being passed to the kernel,
@@ -203,6 +207,10 @@ class NotebookApp(BaseIPythonApplication):
open_browser = Bool(True, config=True,
help="Whether to open in a browser after starting.")
+
+ read_only = Bool(False, config=True,
+ help="Whether to prevent editing/execution of notebooks."
+ )
def get_ws_url(self):
"""Return the WebSocket URL for this server."""
@@ -282,7 +290,7 @@ class NotebookApp(BaseIPythonApplication):
# Try random ports centered around the default.
from random import randint
n = 50 # Max number of attempts, keep reasonably large.
- for port in [self.port] + [self.port + randint(-2*n, 2*n) for i in range(n)]:
+ for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
try:
self.http_server.listen(port, self.ip)
except socket.error, e: