CVE-2024-30265: Insufficient Information (NVD-CWE-noinfo)
value: Collabora Online is a collaborative online office suite based on LibreOffice technology. Any deployment of voilà dashboard allow local file inclusion. Any file on a filesystem that is readable by the user that runs the voilà dashboard server can be downloaded by someone with network access to the server. Whether this still requires authentication depends on how voilà is deployed. This issue has been patched in 0.2.17, 0.3.8, 0.4.4 and 0.5.6.

CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-03T23:15Z
Repository: https://github.com/voila-dashboards/voila
Language: Python
Files changed: voila/app.py, voila/handler.py, voila/paths.py, voila/server_extension.py, voila/static/main.js, voila/templates/page.html, voila/templates/tree.html, voila/treehandler.py, voila/watchdog.py
Commit date: 2018-09-06 12:50:35 +0200
Commit message: refactor: reuse voila to be a server extension + tree view + autoreload

The voila handler can be used both in the standalone version, as well as
jupyter server extension. The three handler which is based on the notebook
code shows a list of directories and notebooks.
For development purposes the watchdog handler will send a reload message
over the websocket when either the javascript, templates, or the notebook
file is modified, or when the server autoreloads due to code changes.
(Use with `voila --autoreload=True`)

Diff: diff --git a/voila/app.py b/voila/app.py
index a9ccb2d..f27e4d2 100644
--- a/voila/app.py
+++ b/voila/app.py
@@ -5,61 +5,29 @@ ioloop.install()
 import os
 import shutil
 import tempfile
 import json
 import logging
+import gettext
+
+import jinja2
 
 import tornado.ioloop
 import tornado.web
 
-from pathlib import Path
-
 from traitlets.config.application import Application
 from traitlets import Unicode, Integer, Bool, default
 
-from jupyter_server.utils import url_path_join, url_escape
-from jupyter_server.base.handlers import JupyterHandler
 from jupyter_server.services.kernels.kernelmanager import MappingKernelManager
-from jupyter_server.services.kernels.handlers import KernelHandler, MainKernelHandler, ZMQChannelsHandler
-
-from jupyter_client.jsonutil import date_default
-
-import nbformat
-from nbconvert.preprocessors.execute import executenb
-from nbconvert import HTMLExporter
-
-ROOT = Path(os.path.dirname(__file__))
-DEFAULT_STATIC_ROOT = ROOT / 'static'
-TEMPLATE_ROOT = ROOT / 'templates'
-
-class VoilaHandler(JupyterHandler):
-
-    def initialize(self, notebook=None, strip_sources=False):
-        self.notebook = notebook
-        self.strip_sources = strip_sources
-
-    @tornado.web.authenticated
-    @tornado.gen.coroutine
-    def get(self):
+from jupyter_server.services.kernels.handlers import KernelHandler, ZMQChannelsHandler
+from jupyter_server.base.handlers import path_regex
+from jupyter_server.services.contents.largefilemanager import LargeFileManager
 
-        # Ignore requested kernel name and make use of the one specified in the notebook.
-        kernel_name = self.notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)
-
-        # Launch kernel and execute notebook.
-        kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
-        km = self.kernel_manager.get_kernel(kernel_id)
-        result = executenb(self.notebook, km=km)
-
-        # render notebook to html
-        resources = dict(kernel_id=kernel_id)
-        html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
-                                       exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
-                                      ).from_notebook_node(result, resources=resources)
-
-        # Compose reply
-        self.set_header('Content-Type', 'text/html')
-        self.write(html)
+from .paths import ROOT, STATIC_ROOT, TEMPLATE_ROOT
+from .handler import VoilaHandler
+from .treehandler import VoilaTreeHandler
+from .watchdog import WatchDogHandler
 
 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
 
 
 class Voila(Application):
@@ -71,30 +39,36 @@ class Voila(Application):
         This launches a stand-alone server for read-only notebooks.
         """
     )
     option_description = Unicode(
         """
-        notebook_filename:
+        notebook_path:
             File name of the Jupyter notebook to display.
         """
     )
     notebook_filename = Unicode()
     strip_sources = Bool(True, help='Strip sources from rendered html').tag(config=True)
     port = Integer(
         8866,
         config=True,
         help='Port of the voila server. Default 8866.'
     )
+    autoreload = Bool(
+        False,
+        config=True,
+        help='Will autoreload to server and the page when a template, js file or Python code changes'
+    )
     static_root = Unicode(
-        str(DEFAULT_STATIC_ROOT),
+        str(STATIC_ROOT),
         config=True,
         help='Directory holding static assets (HTML, JS and CSS files).'
     )
     aliases = {
         'port': 'Voila.port',
         'static': 'Voila.static_root',
-        'strip_sources': 'Voila.strip_sources'
+        'strip_sources': 'Voila.strip_sources',
+        'autoreload': 'Voila.autoreload'
     }
     connection_dir_root = Unicode(
         config=True,
         help=(
             'Location of temporary connection files. Defaults '
@@ -114,18 +88,11 @@ class Voila(Application):
     def _default_log_level(self):
         return logging.INFO
 
     def parse_command_line(self, argv=None):
         super(Voila, self).parse_command_line(argv)
-        try:
-            notebook_filename = self.extra_args[0]
-        except IndexError:
-            self.log.critical('Bad command line parameters.')
-            self.log.critical('Missing NOTEBOOK_FILENAME parameter.')
-            self.log.critical('Run `voila --help` for help on command line parameters.')
-            exit(1)
-        self.notebook_filename = notebook_filename
+        self.notebook_path = self.extra_args[0] if len(self.extra_args) == 1 else None
 
     def start(self):
         connection_dir = tempfile.mkdtemp(
             prefix='voila_',
             dir=self.connection_dir_root
@@ -141,37 +108,58 @@ class Voila(Application):
                 'custom_message',
                 'shutdown_request'
             ]
         )
 
-        notebook = nbformat.read(self.notebook_filename, as_version=4)
-
         handlers = [
-            (
-                r'/',
-                VoilaHandler,
-                {
-                    'notebook': notebook,
-                    'strip_sources': self.strip_sources
-                }
-            ),
             (r'/api/kernels/%s' % _kernel_id_regex, KernelHandler),
             (r'/api/kernels/%s/channels' % _kernel_id_regex, ZMQChannelsHandler),
             (
-                r"/static/(.*)",
+                r"/voila/static/(.*)",
                 tornado.web.StaticFileHandler,
                 {
                     'path': self.static_root,
                     'default_filename': 'index.html'
                 }
             )
         ]
 
+        if self.notebook_path:
+            handlers.append((
+                r'/',
+                VoilaHandler,
+                {
+                    'notebook_path': self.notebook_path,
+                    'strip_sources': self.strip_sources
+                }
+            ))
+        else:
+            handlers.extend([
+                ('/', VoilaTreeHandler),
+                ('/voila/tree' + path_regex, VoilaTreeHandler),
+                ('/voila/render' + path_regex, VoilaHandler, {'strip_sources': self.strip_sources}),
+            ])
+        if self.autoreload:
+            handlers.append(('/voila/watchdog' + path_regex, WatchDogHandler))
+
+        jenv_opt = {"autoescape": True}  # we might want extra options via cmd line like notebook server
+        env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
+        nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
+        env.install_gettext_translations(nbui, newstyle=False)
+
+        contents_manager = LargeFileManager()  # TODO: make this configurable like notebook
+
+
         app = tornado.web.Application(
             handlers,
             kernel_manager=kernel_manager,
-            allow_remote_access=True
+            allow_remote_access=True,
+            autoreload=self.autoreload,
+            voila_jinja2_env=env,
+            static_path='/',
+            server_root_dir='/',
+            contents_manager=contents_manager
         )
 
         app.listen(self.port)
         self.log.info(f'Voila listening on port {self.port}.')
 
diff --git a/voila/handler.py b/voila/handler.py
new file mode 100644
index 0000000..6a9a89f
--- /dev/null
+++ b/voila/handler.py
@@ -0,0 +1,45 @@
+import tornado.web
+
+from jupyter_server.base.handlers import JupyterHandler
+
+import nbformat
+from nbconvert.preprocessors.execute import executenb
+from nbconvert import HTMLExporter
+
+from .paths import TEMPLATE_ROOT
+
+
+class VoilaHandler(JupyterHandler):
+    def initialize(self, notebook_path=None, strip_sources=True):
+        self.notebook_path = notebook_path
+        self.strip_sources = strip_sources
+
+    @tornado.web.authenticated
+    @tornado.gen.coroutine
+    def get(self, path=None):
+        if path:
+            path = path.strip('/')  # remove leading /
+            path += '.ipynb'  # when used as a jupyter server extension, we don't use the extension
+        # if the handler got a notebook_path argument, always serve that
+        notebook_path = self.notebook_path or path
+
+        notebook = nbformat.read(notebook_path, as_version=4)
+
+        # Ignore requested kernel name and make use of the one specified in the notebook.
+        kernel_name = notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)
+
+        # Launch kernel and execute notebook.
+        kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
+        km = self.kernel_manager.get_kernel(kernel_id)
+        result = executenb(notebook, km=km)
+
+        # render notebook to html
+        resources = dict(kernel_id=kernel_id)
+        html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
+                                       exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
+                                      ).from_notebook_node(result, resources=resources)
+
+        # Compose reply
+        self.set_header('Content-Type', 'text/html')
+        self.write(html)
+
diff --git a/voila/paths.py b/voila/paths.py
new file mode 100644
index 0000000..93b7365
--- /dev/null
+++ b/voila/paths.py
@@ -0,0 +1,7 @@
+import os
+from pathlib import Path
+
+ROOT = Path(os.path.dirname(__file__))
+STATIC_ROOT = ROOT / 'static'
+TEMPLATE_ROOT = ROOT / 'templates'
+
diff --git a/voila/server_extension.py b/voila/server_extension.py
new file mode 100644
index 0000000..491dbd0
--- /dev/null
+++ b/voila/server_extension.py
@@ -0,0 +1,37 @@
+import os
+import gettext
+from pathlib import Path
+
+from jinja2 import Environment, FileSystemLoader
+
+import tornado.web
+
+from jupyter_server.utils import url_path_join
+from jupyter_server.base.handlers import path_regex
+
+from .paths import ROOT, TEMPLATE_ROOT, STATIC_ROOT
+from .handler import VoilaHandler
+from .treehandler import VoilaTreeHandler
+from .watchdog import WatchDogHandler
+
+
+def load_jupyter_server_extension(server_app):
+    web_app = server_app.web_app
+
+    jenv_opt = {"autoescape": True}
+    env = Environment(loader=FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
+    web_app.settings['voila_jinja2_env'] = env
+
+    nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
+    env.install_gettext_translations(nbui, newstyle=False)
+
+    host_pattern = '.*$'
+    web_app.add_handlers(host_pattern, [
+        (url_path_join(web_app.settings['base_url'], '/voila/render' + path_regex), VoilaHandler),
+        (url_path_join(web_app.settings['base_url'], '/voila/watchdog' + path_regex), WatchDogHandler),
+        (url_path_join(web_app.settings['base_url'], '/voila'), VoilaTreeHandler),
+        (url_path_join(web_app.settings['base_url'], '/voila/tree' + path_regex), VoilaTreeHandler),
+        (url_path_join(web_app.settings['base_url'], '/voila/static/(.*)'),  tornado.web.StaticFileHandler,
+            {'path': str(STATIC_ROOT)})
+
+    ])
\ No newline at end of file
diff --git a/voila/static/main.js b/voila/static/main.js
index 0a85525..930f05e 100644
--- a/voila/static/main.js
+++ b/voila/static/main.js
@@ -7,11 +7,11 @@ var scripts = document.getElementsByTagName('script');
 Array.prototype.forEach.call(scripts, (script) => {
     kernel_id = script.getAttribute('data-jupyter-kernel-id') || kernel_id;
 })
 
 requirejs.config({
-    baseUrl: 'static/dist'
+    baseUrl: '/voila/static/dist'
 })
 
 require(['libwidgets'], function(lib) {
     var BASEURL = window.location.href
 
@@ -23,10 +23,25 @@ require(['libwidgets'], function(lib) {
         WSURL = 'ws://' + window.location.host
     }
 
     var widgetApp = new lib.WidgetApplication(BASEURL, WSURL, lib.requireLoader, kernel_id);
 
+    var path = window.location.pathname.substr(14);
+    var wsWatchdog = new WebSocket(WSURL + '/voila/watchdog/' + path);
+    wsWatchdog.onmessage = (evt) => {
+        var msg = JSON.parse(evt.data)
+        console.log('msg', msg)
+        if(msg.type == 'reload') {
+            var timeout = 0;
+            if(msg.delay == 'long')
+                timeout = 1000;
+            setTimeout(() => {
+                location.href = location.href;
+            }, timeout)
+        }
+    }
+
     window.addEventListener("beforeunload", function (e) {
         widgetApp.cleanWidgets();
     });
 
     if (document.readyState === 'complete') {
diff --git a/voila/templates/page.html b/voila/templates/page.html
new file mode 100644
index 0000000..9f5c3db
--- /dev/null
+++ b/voila/templates/page.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+    <meta charset="utf-8">
+
+    <title>{% block title %}Voila{% endblock %}</title>
+    {% block favicon %}<link id="favicon" rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    
+    {% block stylesheet %}
+    <!-- <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/> -->
+    {% endblock %}
+    <!-- <link rel="stylesheet" href="{{ base_url }}custom/custom.css" type="text/css" /> -->
+    <link rel="stylesheet" href="https://unpkg.com/font-awesome@4.5.0/css/font-awesome.min.css" type="text/css" />
+    {% block meta %}
+    {% endblock %}
+
+</head>
+
+<body class="{% block bodyclasses %}{% endblock %}"
+ {% block params %}
+  {% if logged_in and token %}
+    data-jupyter-api-token="{{token | urlencode}}"
+  {% endif %}
+ {% endblock params %}
+dir="ltr">
+
+<noscript>
+    <div id='noscript'>
+      {% trans %}Voila requires JavaScript.{% endtrans %}<br>
+      {% trans %}Please enable it to proceed. {% endtrans %}
+  </div>
+</noscript>
+
+<div id="header">
+  <div id="header-container" class="container">
+  {% block login_widget %}
+
+    <span id="login_widget">
+      {% if logged_in %}
+        <button id="logout" class="btn btn-sm navbar-btn">{% trans %}Logout{% endtrans %}</button>
+      {% elif login_available and not logged_in %}
+        <button id="login" class="btn btn-sm navbar-btn">{% trans %}Login{% endtrans %}</button>
+      {% endif %}
+    </span>
+
+  {% endblock %}
+  
+  </div>
+  <div class="header-bar"></div>
+
+  {% block header %}
+  {% endblock %}
+</div>
+
+<div id="site">
+{% block site %}
+{% endblock %}
+</div>
+
+{% block after_site %}
+{% endblock %}
+
+{% block script %}
+{% endblock %}
+
+<script type='text/javascript'>
+  function _remove_token_from_url() {
+    if (window.location.search.length <= 1) {
+      return;
+    }
+    var search_parameters = window.location.search.slice(1).split('&');
+    for (var i = 0; i < search_parameters.length; i++) {
+      if (search_parameters[i].split('=')[0] === 'token') {
+        // remote token from search parameters
+        search_parameters.splice(i, 1);
+        var new_search = '';
+        if (search_parameters.length) {
+          new_search = '?' + search_parameters.join('&');
+        }
+        var new_url = window.location.origin + 
+                      window.location.pathname + 
+                      new_search + 
+                      window.location.hash;
+        window.history.replaceState({}, "", new_url);
+        return;
+      }
+    }
+  }
+  _remove_token_from_url();
+</script>
+</body>
+
+</html>
diff --git a/voila/templates/tree.html b/voila/templates/tree.html
new file mode 100644
index 0000000..45b886a
--- /dev/null
+++ b/voila/templates/tree.html
@@ -0,0 +1,53 @@
+{% extends "page.html" %}
+
+{% block title %}{{page_title}}{% endblock %}
+
+
+{% block params %}
+{{super()}}
+data-base-url="{{base_url | urlencode}}"
+data-notebook-path="{{notebook_path | urlencode}}"
+data-terminals-available="{{terminals_available}}"
+data-server-root="{{server_root}}"
+{% endblock %}
+
+{% block headercontainer %}
+<span class="flex-spacer"></span>
+{% endblock %}
+
+{% block site %}
+
+
+<!-- <ul class="breadcrumb">
+  <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-folder" alt="folder icon"></i></a></li>
+{% for crumb in breadcrumbs[1:] %}
+  <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
+{% endfor %}
+</ul>
+ -->
+
+<ul class="voila-notebooks">
+  {% if breadcrumbs|length > 1: %}
+    <li><a href="{{breadcrumbs[-2][0]}}"><i class="fa fa-folder"></i>..</a></li>
+  {% endif %}
+
+  {% for content in contents.content %}
+    {% if content.type == 'notebook' %}
+      <li><a href="/voila/render/{{content.path.rpartition('.')[0]}}"><i class="fa fa-book"></i>{{content.name.rpartition('.')[0]}}</a></li>
+    {% endif %}
+    {% if content.type == 'directory' %}
+      <li><a href="/voila/tree/{{content.path}}"><i class="fa fa-folder"></i>{{content.name}}</a></li>
+    {% endif %}
+  {% endfor %}
+</ul>
+
+
+
+
+{% endblock %}
+
+{% block script %}
+    {{super()}}
+{% endblock %}
+
+
diff --git a/voila/treehandler.py b/voila/treehandler.py
new file mode 100644
index 0000000..3ec6ace
--- /dev/null
+++ b/voila/treehandler.py
@@ -0,0 +1,60 @@
+from jupyter_server.base.handlers import JupyterHandler
+from jupyter_server.utils import url_path_join, url_escape
+
+class VoilaTreeHandler(JupyterHandler):
+    def get_template(self, name):
+        """Return the jinja template object for a given name"""
+        return self.settings['voila_jinja2_env'].get_template(name)
+
+    def generate_breadcrumbs(self, path):
+        breadcrumbs = [(url_path_join(self.base_url, 'voila/tree'), '')]
+        parts = path.split('/')
+        for i in range(len(parts)):
+            if parts[i]:
+                link = url_path_join(self.base_url, 'voila/tree',
+                    url_escape(url_path_join(*parts[:i+1])),
+                )
+                breadcrumbs.append((link, parts[i]))
+        return breadcrumbs
+
+    def generate_page_title(self, path):
+        parts = path.split('/')
+        if len(parts) > 3:  # not too many parts
+            parts = parts[-2:]
+        page_title = url_path_join(*parts)
+        if page_title:
+            return page_title+'/'
+        else:
+            return 'Voila Home'
+
+    def get(self, path=''):
+        cm = self.contents_manager
+        print(path, cm)
+
+        if cm.dir_exists(path=path):
+            if cm.is_hidden(path) and not cm.allow_hidden:
+                self.log.info("Refusing to serve hidden directory, via 404 Error")
+                raise web.HTTPError(404)
+            breadcrumbs = self.generate_breadcrumbs(path)
+            page_title = self.generate_page_title(path)
+            contents = cm.get(path)
+            self.write(self.render_template('tree.html',
+                page_title=page_title,
+                notebook_path=path,
+                breadcrumbs=breadcrumbs,
+                contents=contents,
+                terminals_available=False,
+                server_root=self.settings['server_root_dir'],
+            ))
+        elif cm.file_exists(path):
+            # it's not a directory, we have redirecting to do
+            model = cm.get(path, content=False)
+            # redirect to /api/notebooks if it's a notebook, otherwise /api/files
+            service = 'notebooks' if model['type'] == 'notebook' else 'files'
+            url = url_path_join(
+                self.base_url, service, url_escape(path),
+            )
+            self.log.debug("Redirecting %s to %s", self.request.path, url)
+            self.redirect(url)
+        else:
+            raise web.HTTPError(404)
diff --git a/voila/watchdog.py b/voila/watchdog.py
new file mode 100644
index 0000000..08fa2a8
--- /dev/null
+++ b/voila/watchdog.py
@@ -0,0 +1,68 @@
+import asyncio
+
+import watchdog.events
+import watchdog.observers
+
+import tornado.websocket
+import tornado.ioloop
+
+from .paths import ROOT
+
+# we cache event handler for watchdogs not to waste resources
+event_handlers = {}
+
+class WatchDogEventHandler(watchdog.events.RegexMatchingEventHandler):
+    def __init__(self, *args, **kwargs):
+        super(WatchDogEventHandler, self).__init__(*args, **kwargs)
+        self.listeners = []
+
+    def on_any_event(self, event):
+        try:
+            asyncio.get_event_loop()
+        except RuntimeError:
+            asyncio.set_event_loop(asyncio.new_event_loop())
+        for listener in self.listeners:
+            listener()
+
+
+
+class WatchDogHandler(tornado.websocket.WebSocketHandler):
+    #@tornado.gen.coroutine
+    def open(self, path=''):
+        self.callback = tornado.ioloop.PeriodicCallback(lambda: self.ping(''), 6000)
+        path = path.strip('/') + '.ipynb'
+        if path not in event_handlers:
+            watchdog_observer = watchdog.observers.Observer()
+            # sometimes useful to add this when triggering does not work
+            # from watchdog.events import LoggingEventHandler
+            # logging_handler = LoggingEventHandler()
+            # watchdog_observer.schedule(logging_handler, '.', recursive=True)
+            
+            handler = WatchDogEventHandler(regexes=['\\./' + path])
+            watchdog_observer.schedule(handler, '.', recursive=True)
+            
+            handler = WatchDogEventHandler(regexes=[str(ROOT) +r'/templates/.*', str(ROOT / 'static/main.js'), str(ROOT / 'static/dist/libwidgets.js')])
+            watchdog_observer.schedule(handler, str(ROOT), recursive=True)
+            
+            watchdog_observer.start()
+            event_handlers[path] = handler
+            
+            tornado.autoreload.add_reload_hook(self._on_reload)
+        self.handler = event_handlers[path]
+        self.handler.listeners.append(self)
+
+    def __call__(self):
+        self.write_message({'type': 'reload', 'delay': 'no'})
+
+    def on_close(self):
+        self.handler.listeners.remove(self)
+
+    def _on_reload(self):
+        try:
+            self.write_message({'type': 'reload', 'delay': 'long'})
+        except tornado.websocket.WebSocketClosedError:
+            print('a websocket was already closed')
+
+
+    def check_origin(self, origin):
+        return True

Vulnerable code: from pathlib import Path

from jupyter_server.utils import url_path_join, url_escape
from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.services.kernels.handlers import KernelHandler, MainKernelHandler, ZMQChannelsHandler

from jupyter_client.jsonutil import date_default

import nbformat
from nbconvert.preprocessors.execute import executenb
from nbconvert import HTMLExporter

ROOT = Path(os.path.dirname(__file__))
DEFAULT_STATIC_ROOT = ROOT / 'static'
TEMPLATE_ROOT = ROOT / 'templates'

class VoilaHandler(JupyterHandler):

    def initialize(self, notebook=None, strip_sources=False):
        self.notebook = notebook
        self.strip_sources = strip_sources

    @tornado.web.authenticated
    @tornado.gen.coroutine
    def get(self):
        # Ignore requested kernel name and make use of the one specified in the notebook.
        kernel_name = self.notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)

        # Launch kernel and execute notebook.
        kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
        km = self.kernel_manager.get_kernel(kernel_id)
        result = executenb(self.notebook, km=km)

        # render notebook to html
        resources = dict(kernel_id=kernel_id)
        html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
                                       exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
                                      ).from_notebook_node(result, resources=resources)

        # Compose reply
        self.set_header('Content-Type', 'text/html')
        self.write(html)
        notebook_filename:
        str(DEFAULT_STATIC_ROOT),
        'strip_sources': 'Voila.strip_sources'
        try:
            notebook_filename = self.extra_args[0]
        except IndexError:
            self.log.critical('Bad command line parameters.')
            self.log.critical('Missing NOTEBOOK_FILENAME parameter.')
            self.log.critical('Run `voila --help` for help on command line parameters.')
            exit(1)
        self.notebook_filename = notebook_filename
        notebook = nbformat.read(self.notebook_filename, as_version=4)

            (
                r'/',
                VoilaHandler,
                {
                    'notebook': notebook,
                    'strip_sources': self.strip_sources
                }
            ),
                r"/static/(.*)",
            allow_remote_access=True
    baseUrl: 'static/dist'
Fixed code: import gettext

import jinja2
from jupyter_server.services.kernels.handlers import KernelHandler, ZMQChannelsHandler
from jupyter_server.base.handlers import path_regex
from jupyter_server.services.contents.largefilemanager import LargeFileManager
from .paths import ROOT, STATIC_ROOT, TEMPLATE_ROOT
from .handler import VoilaHandler
from .treehandler import VoilaTreeHandler
from .watchdog import WatchDogHandler
        notebook_path:
    autoreload = Bool(
        False,
        config=True,
        help='Will autoreload to server and the page when a template, js file or Python code changes'
    )
        str(STATIC_ROOT),
        'strip_sources': 'Voila.strip_sources',
        'autoreload': 'Voila.autoreload'
        self.notebook_path = self.extra_args[0] if len(self.extra_args) == 1 else None
                r"/voila/static/(.*)",
        if self.notebook_path:
            handlers.append((
                r'/',
                VoilaHandler,
                {
                    'notebook_path': self.notebook_path,
                    'strip_sources': self.strip_sources
                }
            ))
        else:
            handlers.extend([
                ('/', VoilaTreeHandler),
                ('/voila/tree' + path_regex, VoilaTreeHandler),
                ('/voila/render' + path_regex, VoilaHandler, {'strip_sources': self.strip_sources}),
            ])
        if self.autoreload:
            handlers.append(('/voila/watchdog' + path_regex, WatchDogHandler))

        jenv_opt = {"autoescape": True}  # we might want extra options via cmd line like notebook server
        env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
        nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
        env.install_gettext_translations(nbui, newstyle=False)

        contents_manager = LargeFileManager()  # TODO: make this configurable like notebook


            allow_remote_access=True,
            autoreload=self.autoreload,
            voila_jinja2_env=env,
            static_path='/',
            server_root_dir='/',
            contents_manager=contents_manager
import tornado.web

from jupyter_server.base.handlers import JupyterHandler

import nbformat
from nbconvert.preprocessors.execute import executenb
from nbconvert import HTMLExporter

from .paths import TEMPLATE_ROOT


class VoilaHandler(JupyterHandler):
    def initialize(self, notebook_path=None, strip_sources=True):
        self.notebook_path = notebook_path
        self.strip_sources = strip_sources

    @tornado.web.authenticated
    @tornado.gen.coroutine
    def get(self, path=None):
        if path:
            path = path.strip('/')  # remove leading /
            path += '.ipynb'  # when used as a jupyter server extension, we don't use the extension
        # if the handler got a notebook_path argument, always serve that
        notebook_path = self.notebook_path or path

        notebook = nbformat.read(notebook_path, as_version=4)

        # Ignore requested kernel name and make use of the one specified in the notebook.
        kernel_name = notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)

        # Launch kernel and execute notebook.
        kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
        km = self.kernel_manager.get_kernel(kernel_id)
        result = executenb(notebook, km=km)

        # render notebook to html
        resources = dict(kernel_id=kernel_id)
        html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
                                       exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
                                      ).from_notebook_node(result, resources=resources)

        # Compose reply
        self.set_header('Content-Type', 'text/html')
        self.write(html)

import os
from pathlib import Path

ROOT = Path(os.path.dirname(__file__))
STATIC_ROOT = ROOT / 'static'
TEMPLATE_ROOT = ROOT / 'templates'

import os
import gettext
from pathlib import Path

from jinja2 import Environment, FileSystemLoader

import tornado.web

from jupyter_server.utils import url_path_join
from jupyter_server.base.handlers import path_regex

from .paths import ROOT, TEMPLATE_ROOT, STATIC_ROOT
from .handler import VoilaHandler
from .treehandler import VoilaTreeHandler
from .watchdog import WatchDogHandler


def load_jupyter_server_extension(server_app):
    web_app = server_app.web_app

    jenv_opt = {"autoescape": True}
    env = Environment(loader=FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
    web_app.settings['voila_jinja2_env'] = env

    nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
    env.install_gettext_translations(nbui, newstyle=False)

    host_pattern = '.*$'
    web_app.add_handlers(host_pattern, [
        (url_path_join(web_app.settings['base_url'], '/voila/render' + path_regex), VoilaHandler),
        (url_path_join(web_app.settings['base_url'], '/voila/watchdog' + path_regex), WatchDogHandler),
        (url_path_join(web_app.settings['base_url'], '/voila'), VoilaTreeHandler),
        (url_path_join(web_app.settings['base_url'], '/voila/tree' + path_regex), VoilaTreeHandler),
        (url_path_join(web_app.settings['base_url'], '/voila/static/(.*)'),  tornado.web.StaticFileHandler,
            {'path': str(STATIC_ROOT)})

    ])
    baseUrl: '/voila/static/dist'
    var path = window.location.pathname.substr(14);
    var wsWatchdog = new WebSocket(WSURL + '/voila/watchdog/' + path);
    wsWatchdog.onmessage = (evt) => {
        var msg = JSON.parse(evt.data)
        console.log('msg', msg)
        if(msg.type == 'reload') {
            var timeout = 0;
            if(msg.delay == 'long')
                timeout = 1000;
            setTimeout(() => {
                location.href = location.href;
            }, timeout)
        }
    }

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">

    <title>{% block title %}Voila{% endblock %}</title>
    {% block favicon %}<link id="favicon" rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
    {% block stylesheet %}
    <!-- <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/> -->
    {% endblock %}
    <!-- <link rel="stylesheet" href="{{ base_url }}custom/custom.css" type="text/css" /> -->
    <link rel="stylesheet" href="https://unpkg.com/font-awesome@4.5.0/css/font-awesome.min.css" type="text/css" />
    {% block meta %}
    {% endblock %}

</head>

<body class="{% block bodyclasses %}{% endblock %}"
 {% block params %}
  {% if logged_in and token %}
    data-jupyter-api-token="{{token | urlencode}}"
  {% endif %}
 {% endblock params %}
dir="ltr">

<noscript>
    <div id='noscript'>
      {% trans %}Voila requires JavaScript.{% endtrans %}<br>
      {% trans %}Please enable it to proceed. {% endtrans %}
  </div>
</noscript>

<div id="header">
  <div id="header-container" class="container">
  {% block login_widget %}

    <span id="login_widget">
      {% if logged_in %}
        <button id="logout" class="btn btn-sm navbar-btn">{% trans %}Logout{% endtrans %}</button>
      {% elif login_available and not logged_in %}
        <button id="login" class="btn btn-sm navbar-btn">{% trans %}Login{% endtrans %}</button>
      {% endif %}
    </span>

  {% endblock %}
  
  </div>
  <div class="header-bar"></div>

  {% block header %}
  {% endblock %}
</div>

<div id="site">
{% block site %}
{% endblock %}
</div>

{% block after_site %}
{% endblock %}

{% block script %}
{% endblock %}

<script type='text/javascript'>
  function _remove_token_from_url() {
    if (window.location.search.length <= 1) {
      return;
    }
    var search_parameters = window.location.search.slice(1).split('&');
    for (var i = 0; i < search_parameters.length; i++) {
      if (search_parameters[i].split('=')[0] === 'token') {
        // remote token from search parameters
        search_parameters.splice(i, 1);
        var new_search = '';
        if (search_parameters.length) {
          new_search = '?' + search_parameters.join('&');
        }
        var new_url = window.location.origin + 
                      window.location.pathname + 
                      new_search + 
                      window.location.hash;
        window.history.replaceState({}, "", new_url);
        return;
      }
    }
  }
  _remove_token_from_url();
</script>
</body>

</html>
{% extends "page.html" %}

{% block title %}{{page_title}}{% endblock %}


{% block params %}
{{super()}}
data-base-url="{{base_url | urlencode}}"
data-notebook-path="{{notebook_path | urlencode}}"
data-terminals-available="{{terminals_available}}"
data-server-root="{{server_root}}"
{% endblock %}

{% block headercontainer %}
<span class="flex-spacer"></span>
{% endblock %}

{% block site %}


<!-- <ul class="breadcrumb">
  <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-folder" alt="folder icon"></i></a></li>
{% for crumb in breadcrumbs[1:] %}
  <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
{% endfor %}
</ul>
 -->

<ul class="voila-notebooks">
  {% if breadcrumbs|length > 1: %}
    <li><a href="{{breadcrumbs[-2][0]}}"><i class="fa fa-folder"></i>..</a></li>
  {% endif %}

  {% for content in contents.content %}
    {% if content.type == 'notebook' %}
      <li><a href="/voila/render/{{content.path.rpartition('.')[0]}}"><i class="fa fa-book"></i>{{content.name.rpartition('.')[0]}}</a></li>
    {% endif %}
    {% if content.type == 'directory' %}
      <li><a href="/voila/tree/{{content.path}}"><i class="fa fa-folder"></i>{{content.name}}</a></li>
    {% endif %}
  {% endfor %}
</ul>




{% endblock %}

{% block script %}
    {{super()}}
{% endblock %}


from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.utils import url_path_join, url_escape

class VoilaTreeHandler(JupyterHandler):
    def get_template(self, name):
        """Return the jinja template object for a given name"""
        return self.settings['voila_jinja2_env'].get_template(name)

    def generate_breadcrumbs(self, path):
        breadcrumbs = [(url_path_join(self.base_url, 'voila/tree'), '')]
        parts = path.split('/')
        for i in range(len(parts)):
            if parts[i]:
                link = url_path_join(self.base_url, 'voila/tree',
                    url_escape(url_path_join(*parts[:i+1])),
                )
                breadcrumbs.append((link, parts[i]))
        return breadcrumbs

    def generate_page_title(self, path):
        parts = path.split('/')
        if len(parts) > 3:  # not too many parts
            parts = parts[-2:]
        page_title = url_path_join(*parts)
        if page_title:
            return page_title+'/'
        else:
            return 'Voila Home'

    def get(self, path=''):
        cm = self.contents_manager
        print(path, cm)

        if cm.dir_exists(path=path):
            if cm.is_hidden(path) and not cm.allow_hidden:
                self.log.info("Refusing to serve hidden directory, via 404 Error")
                raise web.HTTPError(404)
            breadcrumbs = self.generate_breadcrumbs(path)
            page_title = self.generate_page_title(path)
            contents = cm.get(path)
            self.write(self.render_template('tree.html',
                page_title=page_title,
                notebook_path=path,
                breadcrumbs=breadcrumbs,
                contents=contents,
                terminals_available=False,
                server_root=self.settings['server_root_dir'],
            ))
        elif cm.file_exists(path):
            # it's not a directory, we have redirecting to do
            model = cm.get(path, content=False)
            # redirect to /api/notebooks if it's a notebook, otherwise /api/files
            service = 'notebooks' if model['type'] == 'notebook' else 'files'
            url = url_path_join(
                self.base_url, service, url_escape(path),
            )
            self.log.debug("Redirecting %s to %s", self.request.path, url)
            self.redirect(url)
        else:
            raise web.HTTPError(404)
import asyncio

import watchdog.events
import watchdog.observers

import tornado.websocket
import tornado.ioloop

from .paths import ROOT

# we cache event handler for watchdogs not to waste resources
event_handlers = {}

class WatchDogEventHandler(watchdog.events.RegexMatchingEventHandler):
    def __init__(self, *args, **kwargs):
        super(WatchDogEventHandler, self).__init__(*args, **kwargs)
        self.listeners = []

    def on_any_event(self, event):
        try:
            asyncio.get_event_loop()
        except RuntimeError:
            asyncio.set_event_loop(asyncio.new_event_loop())
        for listener in self.listeners:
            listener()



class WatchDogHandler(tornado.websocket.WebSocketHandler):
    #@tornado.gen.coroutine
    def open(self, path=''):
        self.callback = tornado.ioloop.PeriodicCallback(lambda: self.ping(''), 6000)
        path = path.strip('/') + '.ipynb'
        if path not in event_handlers:
            watchdog_observer = watchdog.observers.Observer()
            # sometimes useful to add this when triggering does not work
            # from watchdog.events import LoggingEventHandler
            # logging_handler = LoggingEventHandler()
            # watchdog_observer.schedule(logging_handler, '.', recursive=True)
            
            handler = WatchDogEventHandler(regexes=['\\./' + path])
            watchdog_observer.schedule(handler, '.', recursive=True)
            
            handler = WatchDogEventHandler(regexes=[str(ROOT) +r'/templates/.*', str(ROOT / 'static/main.js'), str(ROOT / 'static/dist/libwidgets.js')])
            watchdog_observer.schedule(handler, str(ROOT), recursive=True)
            
            watchdog_observer.start()
            event_handlers[path] = handler
            
            tornado.autoreload.add_reload_hook(self._on_reload)
        self.handler = event_handlers[path]
        self.handler.listeners.append(self)

    def __call__(self):
        self.write_message({'type': 'reload', 'delay': 'no'})

    def on_close(self):
        self.handler.listeners.remove(self)

    def _on_reload(self):
        try:
            self.write_message({'type': 'reload', 'delay': 'long'})
        except tornado.websocket.WebSocketClosedError:
            print('a websocket was already closed')


    def check_origin(self, origin):
        return True