1# coding: utf-8 2"""JupyterLab Server handlers""" 3 4# Copyright (c) Jupyter Development Team. 5# Distributed under the terms of the Modified BSD License. 6import os 7from urllib.parse import urlparse 8 9from tornado import template, web 10 11from jupyter_server.extension.handler import ExtensionHandlerMixin, ExtensionHandlerJinjaMixin 12 13from .config import LabConfig, get_page_config, recursive_update 14from .listings_handler import ListingsHandler, fetch_listings 15from .server import FileFindHandler, JupyterHandler 16from .server import url_path_join as ujoin 17from .settings_handler import SettingsHandler 18from .themes_handler import ThemesHandler 19from .translations_handler import TranslationsHandler 20from .workspaces_handler import WorkspacesHandler 21from .licenses_handler import LicensesHandler, LicensesManager 22 23# ----------------------------------------------------------------------------- 24# Module globals 25# ----------------------------------------------------------------------------- 26 27MASTER_URL_PATTERN = '/(?P<mode>{}|doc)(?P<workspace>/workspaces/[a-zA-Z0-9\-\_]+)?(?P<tree>/tree/.*)?' 28 29DEFAULT_TEMPLATE = template.Template(""" 30<!DOCTYPE html> 31<html> 32<head> 33 <meta charset="utf-8"> 34 <title>Error</title> 35</head> 36<body> 37<h2>Cannot find template: "{{name}}"</h2> 38<p>In "{{path}}"</p> 39</body> 40</html> 41""") 42 43 44def is_url(url): 45 """Test whether a string is a full url (e.g. https://nasa.gov) 46 47 https://stackoverflow.com/a/52455972 48 """ 49 try: 50 result = urlparse(url) 51 return all([result.scheme, result.netloc]) 52 except ValueError: 53 return False 54 55 56 57class LabHandler(ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): 58 """Render the JupyterLab View.""" 59 60 @web.authenticated 61 @web.removeslash 62 def get(self, mode = None, workspace = None, tree = None): 63 """Get the JupyterLab html page.""" 64 workspace = 'default' if workspace is None else workspace.replace('/workspaces/','') 65 tree_path = '' if tree is None else tree.replace('/tree/','') 66 67 self.application.store_id = getattr(self.application, 'store_id', 0) 68 config = LabConfig() 69 app = self.extensionapp 70 settings_dir = app.app_settings_dir 71 72 # Handle page config data. 73 page_config = self.settings.setdefault('page_config_data', {}) 74 terminals = self.settings.get('terminals_available', False) 75 server_root = self.settings.get('server_root_dir', '') 76 server_root = server_root.replace(os.sep, '/') 77 base_url = self.settings.get('base_url') 78 79 # Remove the trailing slash for compatibiity with html-webpack-plugin. 80 full_static_url = self.static_url_prefix.rstrip('/') 81 page_config.setdefault('fullStaticUrl', full_static_url) 82 83 page_config.setdefault('terminalsAvailable', terminals) 84 page_config.setdefault('ignorePlugins', []) 85 page_config.setdefault('serverRoot', server_root) 86 page_config['store_id'] = self.application.store_id 87 88 server_root = os.path.normpath(os.path.expanduser(server_root)) 89 try: 90 page_config['preferredDir'] = self.serverapp.preferred_dir 91 page_config['preferredPath'] = self.serverapp.preferred_dir.replace(server_root, "") 92 except Exception: 93 page_config['preferredDir'] = server_root 94 page_config['preferredPath'] = '/' 95 96 self.application.store_id += 1 97 98 mathjax_config = self.settings.get('mathjax_config', 99 'TeX-AMS_HTML-full,Safe') 100 # TODO Remove CDN usage. 101 mathjax_url = self.mathjax_url 102 if not mathjax_url: 103 mathjax_url = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js' 104 105 page_config.setdefault('mathjaxConfig', mathjax_config) 106 page_config.setdefault('fullMathjaxUrl', mathjax_url) 107 108 # Add parameters parsed from the URL 109 if mode == 'doc': 110 page_config['mode'] = 'single-document' 111 else: 112 page_config['mode'] = 'multiple-document' 113 page_config['workspace'] = workspace 114 page_config['treePath'] = tree_path 115 116 # Put all our config in page_config 117 for name in config.trait_names(): 118 page_config[_camelCase(name)] = getattr(app, name) 119 120 # Add full versions of all the urls 121 for name in config.trait_names(): 122 if not name.endswith('_url'): 123 continue 124 full_name = _camelCase('full_' + name) 125 full_url = getattr(app, name) 126 if not is_url(full_url): 127 # Relative URL will be prefixed with base_url 128 full_url = ujoin(base_url, full_url) 129 page_config[full_name] = full_url 130 131 # Update the page config with the data from disk 132 labextensions_path = app.extra_labextensions_path + app.labextensions_path 133 recursive_update(page_config, get_page_config(labextensions_path, settings_dir, logger=self.log)) 134 135 # Write the template with the config. 136 tpl = self.render_template('index.html', page_config=page_config) 137 self.write(tpl) 138 139 140class NotFoundHandler(LabHandler): 141 def render_template(self, name, **ns): 142 if 'page_config' in ns: 143 ns['page_config'] = ns['page_config'].copy() 144 ns['page_config']['notFoundUrl'] = self.request.path 145 return super().render_template(name, **ns) 146 147 148def add_handlers(handlers, extension_app): 149 """Add the appropriate handlers to the web app. 150 """ 151 # Normalize directories. 152 for name in LabConfig.class_trait_names(): 153 if not name.endswith('_dir'): 154 continue 155 value = getattr(extension_app, name) 156 setattr(extension_app, name, value.replace(os.sep, '/')) 157 158 # Normalize urls 159 # Local urls should have a leading slash but no trailing slash 160 for name in LabConfig.class_trait_names(): 161 if not name.endswith('_url'): 162 continue 163 value = getattr(extension_app, name) 164 if is_url(value): 165 continue 166 if not value.startswith('/'): 167 value = '/' + value 168 if value.endswith('/'): 169 value = value[:-1] 170 setattr(extension_app, name, value) 171 172 url_pattern = MASTER_URL_PATTERN.format(extension_app.app_url.replace('/', '')) 173 handlers.append((url_pattern, LabHandler)) 174 175 # Cache all or none of the files depending on the `cache_files` setting. 176 no_cache_paths = [] if extension_app.cache_files else ['/'] 177 178 # Handle federated lab extensions. 179 labextensions_path = extension_app.extra_labextensions_path + extension_app.labextensions_path 180 labextensions_url = ujoin(extension_app.labextensions_url, "(.*)") 181 handlers.append( 182 (labextensions_url, FileFindHandler, { 183 'path': labextensions_path, 184 'no_cache_paths': no_cache_paths 185 })) 186 187 # Handle local settings. 188 if extension_app.schemas_dir: 189 settings_config = { 190 'app_settings_dir': extension_app.app_settings_dir, 191 'schemas_dir': extension_app.schemas_dir, 192 'settings_dir': extension_app.user_settings_dir, 193 'labextensions_path': labextensions_path 194 } 195 196 # Handle requests for the list of settings. Make slash optional. 197 settings_path = ujoin(extension_app.settings_url, '?') 198 handlers.append((settings_path, SettingsHandler, settings_config)) 199 200 # Handle requests for an individual set of settings. 201 setting_path = ujoin(extension_app.settings_url, '(?P<schema_name>.+)') 202 handlers.append((setting_path, SettingsHandler, settings_config)) 203 204 # Handle translations. 205 ## Translations requires settings as the locale source of truth is stored in it 206 if extension_app.translations_api_url: 207 # Handle requests for the list of language packs available. 208 # Make slash optional. 209 translations_path = ujoin(extension_app.translations_api_url, '?') 210 handlers.append((translations_path, TranslationsHandler, settings_config)) 211 212 # Handle requests for an individual language pack. 213 translations_lang_path = ujoin( 214 extension_app.translations_api_url, '(?P<locale>.*)') 215 handlers.append((translations_lang_path, TranslationsHandler, settings_config)) 216 217 # Handle saved workspaces. 218 if extension_app.workspaces_dir: 219 220 workspaces_config = { 221 'path': extension_app.workspaces_dir 222 } 223 224 # Handle requests for the list of workspaces. Make slash optional. 225 workspaces_api_path = ujoin(extension_app.workspaces_api_url, '?') 226 handlers.append(( 227 workspaces_api_path, WorkspacesHandler, workspaces_config)) 228 229 # Handle requests for an individually named workspace. 230 workspace_api_path = ujoin(extension_app.workspaces_api_url, '(?P<space_name>.+)') 231 handlers.append(( 232 workspace_api_path, WorkspacesHandler, workspaces_config)) 233 234 # Handle local listings. 235 236 settings_config = extension_app.settings.get('config', {}).get('LabServerApp', {}) 237 blocked_extensions_uris = settings_config.get('blocked_extensions_uris', '') 238 allowed_extensions_uris = settings_config.get('allowed_extensions_uris', '') 239 240 if (blocked_extensions_uris) and (allowed_extensions_uris): 241 print('Simultaneous blocked_extensions_uris and allowed_extensions_uris is not supported. Please define only one of those.') 242 import sys 243 sys.exit(-1) 244 245 ListingsHandler.listings_refresh_seconds = settings_config.get('listings_refresh_seconds', 60 * 60) 246 ListingsHandler.listings_request_opts = settings_config.get('listings_request_options', {}) 247 listings_url = ujoin(extension_app.listings_url) 248 listings_path = ujoin(listings_url, '(.*)') 249 250 if blocked_extensions_uris: 251 ListingsHandler.blocked_extensions_uris = set(blocked_extensions_uris.split(',')) 252 if allowed_extensions_uris: 253 ListingsHandler.allowed_extensions_uris = set(allowed_extensions_uris.split(',')) 254 255 fetch_listings(None) 256 257 if len(ListingsHandler.blocked_extensions_uris) > 0 or len(ListingsHandler.allowed_extensions_uris) > 0: 258 from tornado import ioloop 259 ListingsHandler.pc = ioloop.PeriodicCallback( 260 lambda: fetch_listings(None), 261 callback_time=ListingsHandler.listings_refresh_seconds * 1000, 262 jitter=0.1 263 ) 264 ListingsHandler.pc.start() 265 266 handlers.append((listings_path, ListingsHandler, {})) 267 268 # Handle local themes. 269 if extension_app.themes_dir: 270 themes_url = extension_app.themes_url 271 themes_path = ujoin(themes_url, '(.*)') 272 handlers.append(( 273 themes_path, 274 ThemesHandler, 275 { 276 'themes_url': themes_url, 277 'path': extension_app.themes_dir, 278 'labextensions_path': labextensions_path, 279 'no_cache_paths': no_cache_paths 280 } 281 )) 282 283 # Handle licenses. 284 if extension_app.licenses_url: 285 licenses_url = extension_app.licenses_url 286 licenses_path = ujoin(licenses_url, '(.*)') 287 handlers.append(( 288 licenses_path, 289 LicensesHandler, 290 { 291 'manager': LicensesManager(parent=extension_app) 292 } 293 )) 294 295 # Let the lab handler act as the fallthrough option instead of a 404. 296 fallthrough_url = ujoin(extension_app.app_url, r'.*') 297 handlers.append((fallthrough_url, NotFoundHandler)) 298 299 300def _camelCase(base): 301 """Convert a string to camelCase. 302 https://stackoverflow.com/a/20744956 303 """ 304 output = ''.join(x for x in base.title() if x.isalpha()) 305 return output[0].lower() + output[1:] 306