def register_routes(): return ( ( r"^/til/til/(?P<topic>[^_]+)_(?P<slug>[^\.]+)\.md$", lambda request: Response.redirect( "/{topic}/{slug}".format(**request.url_vars), status=301), ), ("^/til/feed.atom$", lambda: Response.redirect("/tils/feed.atom", status=301)), ( "^/til$", lambda request: Response.redirect( "/tils" + (("?" + request.query_string) if request.query_string else ""), status=301, ), ), ( "^/til/search$", lambda request: Response.redirect( "/tils/search" + (("?" + request.query_string) if request.query_string else ""), status=301, ), ), )
async def _tiles_stack(datasette, request, tms): priority_order = await tiles_stack_database_order(datasette) # Try each database in turn for database in priority_order: tile = await load_tile(database, request, tms=tms) if tile is not None: return Response(body=tile, content_type="image/png") return Response(body=PNG_404, content_type="image/png", status=404)
async def _tile(request, datasette, tms): db_name = request.url_vars["db_name"] mbtiles_databases = await detect_mtiles_databases(datasette) if db_name not in mbtiles_databases: raise NotFound("Not a valid mbtiles database") db = datasette.get_database(db_name) tile = await load_tile(db, request, tms) if tile is None: return Response(body=PNG_404, content_type="image/png", status=404) return Response(body=tile, content_type="image/png")
async def manage_db_group(scope, receive, datasette, request): db_name = unquote_plus(request.url_vars["database"]) if not await datasette.permission_allowed( request.actor, "live-permissions-edit", db_name, default=False ): raise Forbidden("Permission denied") db = get_db(datasette) group_id = None results = db["groups"].rows_where("name=?", [f"DB Access: {db_name}"]) for row in results: group_id = row["id"] break assert db_name in datasette.databases, "Non-existant database!" if not group_id and db_name not in BLOCKED_DB_ACTIONS: db["groups"].insert({ "name": f"DB Access: {db_name}", }, pk="id", replace=True) return await manage_db_group(scope, receive, datasette, request) if request.method in ["POST", "DELETE"]: formdata = await request.post_vars() user_id = formdata["user_id"] if request.method == "POST": db["group_membership"].insert({ "group_id": group_id, "user_id": user_id, }, replace=True) elif request.method == "DELETE": db["group_membership"].delete((group_id, user_id)) return Response.text('', status=204) else: raise NotImplementedError(f"Bad method: {request.method}") perms_query = """ select distinct user_id as id, lookup, value, description from group_membership join users on group_membership.user_id = users.id where group_membership.group_id=? """ users = db.execute(perms_query, (group_id,)) return Response.html( await datasette.render_template( "database_management.html", { "database": db_name, "users": users, }, request=request ) )
async def get(self, request): token = request.args.get("token") or "" if not self.ds._root_token: return Response("Root token has already been used", status=403) if secrets.compare_digest(token, self.ds._root_token): self.ds._root_token = None response = Response.redirect("/") response.set_cookie("ds_actor", self.ds.sign({"a": { "id": "root" }}, "actor")) return response else: return Response("Invalid token", status=403)
async def live_config(scope, receive, datasette, request): submit_url = request.path database_name = unquote_plus( request.url_vars.get("database_name", "global")) meta_in_db = True if request.args.get("meta_in_db") else False if meta_in_db: submit_url += '?meta_in_db=true' table_name = "global" perm_args = () if database_name: perm_args = (database_name, ) if not await datasette.permission_allowed( request.actor, "live-config", *perm_args, default=False): raise Forbidden("Permission denied for live-config") if request.method != "POST": # TODO: Decide if we use this or pull saved config metadata = datasette.metadata() if database_name and database_name != "global": metadata = metadata["databases"].get(database_name, {}) return Response.html(await datasette.render_template( "config_editor.html", { "database_name": database_name, "configJSON": json.dumps(metadata), "submit_url": submit_url, }, request=request)) formdata = await request.post_vars() if meta_in_db and database_name in datasette.databases: db_meta = json.loads(formdata["config"]) update_db_metadata(datasette.databases[database_name], db_meta) else: update_live_config_db(datasette, database_name, table_name, formdata["config"]) metadata = datasette.metadata() if database_name != "global": metadata = metadata["databases"][database_name] return Response.html(await datasette.render_template( "config_editor.html", { "database_name": database_name, "message": "Configuration updated successfully!", "status": "success", "configJSON": json.dumps(metadata), "submit_url": submit_url, }, request=request))
async def render(self, templates, request, context): template = self.ds.jinja_env.select_template(templates) select_templates = [ "{}{}".format("*" if template_name == template.name else "", template_name) for template_name in templates ] body_scripts = [] # pylint: disable=no-member for script in pm.hook.extra_body_script( template=template.name, database=context.get("database"), table=context.get("table"), view_name=self.name, datasette=self.ds, ): body_scripts.append(jinja2.Markup(script)) extra_template_vars = {} # pylint: disable=no-member for extra_vars in pm.hook.extra_template_vars( template=template.name, database=context.get("database"), table=context.get("table"), view_name=self.name, request=request, datasette=self.ds, ): if callable(extra_vars): extra_vars = extra_vars() if asyncio.iscoroutine(extra_vars): extra_vars = await extra_vars assert isinstance(extra_vars, dict), "extra_vars is of type {}".format( type(extra_vars)) extra_template_vars.update(extra_vars) return Response.html(await template.render_async({ **context, **{ "app_css_hash": self.ds.app_css_hash(), "select_templates": select_templates, "zip": zip, "body_scripts": body_scripts, "extra_css_urls": self._asset_urls("extra_css_urls", template, context), "extra_js_urls": self._asset_urls("extra_js_urls", template, context), "format_bytes": format_bytes, "database_url": self.database_url, "database_color": self.database_color, }, **extra_template_vars, }))
async def render(self, templates, request, context=None): context = context or {} template = self.ds.jinja_env.select_template(templates) template_context = { **context, **{ "database_url": self.database_url, "csrftoken": request.scope["csrftoken"], "database_color": self.database_color, "show_messages": lambda: self.ds._show_messages(request), "select_templates": [ "{}{}".format( "*" if template_name == template.name else "", template_name) for template_name in templates ], }, } return Response.html(await self.ds.render_template(template, template_context, request=request, view_name=self.name))
async def render(self, templates, request, context=None): context = context or {} template = self.ds.jinja_env.select_template(templates) template_context = { **context, **{ "database_color": self.database_color, "select_templates": [ f"{'*' if template_name == template.name else ''}{template_name}" for template_name in templates ], }, } headers = {} if self.has_json_alternate: alternate_url_json = self.ds.absolute_url( request, self.ds.urls.path( path_with_format(request=request, format="json")), ) template_context["alternate_url_json"] = alternate_url_json headers.update({ "Link": '{}; rel="alternate"; type="application/json+datasette"'. format(alternate_url_json) }) return Response.html( await self.ds.render_template( template, template_context, request=request, view_name=self.name, ), headers=headers, )
async def tiles_stack_explorer(datasette): attribution = "" # Find min/max zoom by looking at the stack priority_order = await tiles_stack_database_order(datasette) min_zooms = [] max_zooms = [] attributions = [] for db in priority_order: metadata = { row["name"]: row["value"] for row in ( await db.execute("select name, value from metadata")).rows } if "minzoom" in metadata: min_zooms.append(int(metadata["minzoom"])) if "maxzoom" in metadata: max_zooms.append(int(metadata["maxzoom"])) # If all attributions are the same, use that - otherwise leave blank if len(set(attributions)) == 1: attribution = attributions[0] min_zoom = min(min_zooms) max_zoom = max(max_zooms) return Response.html(await datasette.render_template( "tiles_stack_explorer.html", { "default_latitude": 0, "default_longitude": 0, "default_zoom": min_zoom, "min_zoom": min_zoom, "max_zoom": max_zoom, "attribution": json.dumps(attribution), }, ))
def login_as_root(datasette, request): # Mainly for the latest.datasette.io demo if request.method == "POST": response = Response.redirect("/") response.set_cookie("ds_actor", datasette.sign({"a": { "id": "root" }}, "actor")) return response return Response.html(""" <form action="{}" method="POST"> <p> <input type="hidden" name="csrftoken" value="{}"> <input type="submit" value="Sign in as root user"></p> </form> """.format(request.path, request.scope["csrftoken"]()))
async def dashboard_chart(request, datasette): await check_permission_instance(request, datasette) config = datasette.plugin_config("datasette-dashboards") or {} slug = urllib.parse.unquote(request.url_vars["slug"]) chart_slug = urllib.parse.unquote(request.url_vars["chart_slug"]) try: dashboard = config[slug] except KeyError: raise NotFound(f"Dashboard not found: {slug}") try: chart = dashboard["charts"][chart_slug] except KeyError: raise NotFound(f"Chart does not exist: {chart_slug}") db = chart.get("db") if db: database = datasette.get_database(db) await check_permission_execute_sql(request, datasette, database) options_keys = get_dashboard_filters_keys(request, dashboard) query_string = generate_dashboard_filters_qs(request, options_keys) fill_chart_query_options(chart, options_keys) return Response.html(await datasette.render_template( "dashboard_chart.html", { "slug": slug, "query_string": query_string, "dashboard": dashboard, "chart": chart, }, ))
async def dashboard_list(request, datasette): await check_permission_instance(request, datasette) config = datasette.plugin_config("datasette-dashboards") or {} return Response.html(await datasette.render_template( "dashboard_list.html", {"dashboards": config}, ))
async def get(self, request, as_format): await self.check_permission(request, "view-instance") if self.needs_request: data = self.data_callback(request) else: data = self.data_callback() if as_format: headers = {} if self.ds.cors: headers["Access-Control-Allow-Origin"] = "*" return Response( json.dumps(data), content_type="application/json; charset=utf-8", headers=headers, ) else: return await self.render( ["show_json.html"], request=request, context={ "filename": self.filename, "data_json": json.dumps(data, indent=4), }, )
async def get(self, request): as_format = request.url_vars["format"] await self.ds.ensure_permissions(request.actor, ["view-instance"]) if self.needs_request: data = self.data_callback(request) else: data = self.data_callback() if as_format: headers = {} if self.ds.cors: add_cors_headers(headers) return Response( json.dumps(data), content_type="application/json; charset=utf-8", headers=headers, ) else: return await self.render( ["show_json.html"], request=request, context={ "filename": self.filename, "data_json": json.dumps(data, indent=4), }, )
def register_routes(): return ( # Homepage (r"^/$", lambda: Response.redirect("/us/pillar-point")), # country/slug - US only for the moment (r"^/us/(?P<slug>[^/]+)$", place_page), )
async def test_response_set_cookie(): events = [] async def send(event): events.append(event) response = Response.redirect("/foo") response.set_cookie("foo", "bar", max_age=10, httponly=True) await response.asgi_send(send) assert [ { "type": "http.response.start", "status": 302, "headers": [ [b"Location", b"/foo"], [b"content-type", b"text/plain"], [ b"set-cookie", b"foo=bar; HttpOnly; Max-Age=10; Path=/; SameSite=lax" ], ], }, { "type": "http.response.body", "body": b"" }, ] == events
async def get(self, request): if not await self.ds.permission_allowed(request.actor, "permissions-debug"): return Response("Permission denied", status=403) return await self.render( ["permissions_debug.html"], request, {"permission_checks": reversed(self.ds._permission_checks)}, )
async def render(self, templates, request, context): template = self.ds.jinja_env.select_template(templates) template_context = { **context, **{ "database_url": self.database_url, "database_color": self.database_color, }, } if (request and request.args.get("_context") and self.ds.config("template_debug")): return Response.html("<pre>{}</pre>".format( jinja2.escape( json.dumps(template_context, default=repr, indent=4)))) return Response.html(await self.ds.render_template(template, template_context, request=request))
async def paprika_recipe_link(request, datasette, rows): row = rows[0] return Response.html(await datasette.render_template( "recipe.html", dict(zip(row.keys(), tuple(row))), request=request, ))
async def view_graphql_schema(request, datasette): database = request.url_vars.get("database") try: datasette.get_database(database) except KeyError: raise NotFound("Database does not exist") schema = await schema_for_database_via_cache(datasette, database=database) return Response.text(print_schema(schema))
async def get(self, request): if not request.actor: return Response.redirect("/") return await self.render( ["logout.html"], request, {"actor": request.actor}, )
async def reconcile(request, datasette): database = request.url_vars["db_name"] table = request.url_vars["db_table"] db = datasette.get_database(database) # get plugin configuration config = datasette.plugin_config( "datasette-reconcile", database=database, table=table ) config = await check_config(config, db, table) # check user can at least view this table await check_permissions( request, [ ("view-table", (database, table)), ("view-database", database), "view-instance", ], datasette, ) # work out if we are looking for queries post_vars = await request.post_vars() queries = post_vars.get("queries", request.args.get("queries")) if queries: queries = json.loads(queries) return Response.json( { q[0]: {"result": q[1]} async for q in reconcile_queries(queries, config, db, table) }, headers={ "Access-Control-Allow-Origin": "*", }, ) # if we're not then just return the service specification return Response.json( service_manifest(config, database, table, datasette, request), headers={ "Access-Control-Allow-Origin": "*", }, )
def redirect(self, request, path, forward_querystring=True, remove_args=None): if request.query_string and "?" not in path and forward_querystring: path = "{}?{}".format(path, request.query_string) if remove_args: path = path_with_removed_args(request, remove_args, path=path) r = Response.redirect(path) r.headers["Link"] = "<{}>; rel=preload".format(path) if self.ds.cors: r.headers["Access-Control-Allow-Origin"] = "*" return r
def handle_exception(datasette, request, exception): datasette._exception_hook_fired = (request, exception) if request.args.get("_custom_error"): return Response.text("_custom_error") elif request.args.get("_custom_error_async"): async def inner(): return Response.text("_custom_error_async") return inner
async def paprika_recipe_route(request, datasette): r = await datasette.client.get( f'paprika/recipes/{request.url_vars["recipe_id"]}.json') row_data = r.json() return Response.html(await datasette.render_template( "recipe.html", dict(zip(row_data["columns"], row_data["rows"][0])), request=request, ))
def robots_txt(datasette): config = datasette.plugin_config("datasette-block-robots") or {} literal = config.get("literal") disallow = [] if literal: return Response.text(literal) disallow = config.get("disallow") or [] if isinstance(disallow, str): disallow = [disallow] allow_only_index = config.get("allow_only_index") if allow_only_index: for database_name in datasette.databases: if database_name != "_internal": disallow.append(datasette.urls.database(database_name)) if not disallow: disallow = ["/"] lines = ["User-agent: *" ] + ["Disallow: {}".format(item) for item in disallow] return Response.text("\n".join(lines))
def redirect(self, request, path, forward_querystring=True, remove_args=None): if request.query_string and "?" not in path and forward_querystring: path = f"{path}?{request.query_string}" if remove_args: path = path_with_removed_args(request, remove_args, path=path) r = Response.redirect(path) r.headers["Link"] = f"<{path}>; rel=preload" if self.ds.cors: add_cors_headers(r.headers) return r
async def get(self, request): token = request.args.get("token") or "" if not self.ds._root_token: return Response("Root token has already been used", status=403) if secrets.compare_digest(token, self.ds._root_token): self.ds._root_token = None cookie = SimpleCookie() cookie["ds_actor"] = self.ds.sign({"id": "root"}, "actor") cookie["ds_actor"]["path"] = "/" response = Response( body="", status=302, headers={ "Location": "/", "set-cookie": cookie.output(header="").lstrip(), }, ) return response else: return Response("Invalid token", status=403)
async def schema_versions(datasette, request): return Response.html( await datasette.render_template( "show_json.html", { "filename": "schema-versions.json", "data_json": json.dumps(await _schema_versions(datasette), indent=4), }, request=request, ) )