diff --git a/datasette/app.py b/datasette/app.py index 84285752..66a7573a 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -97,10 +97,13 @@ CONFIG_OPTIONS = ( Allow .csv?_stream=1 to download all rows (ignoring max_returned_rows) """.strip()), ConfigOption("max_csv_mb", 100, """ - Maximum size allowed for CSV export in MB. Set 0 to disable this limit. + Maximum size allowed for CSV export in MB - set 0 to disable this limit """.strip()), ConfigOption("truncate_cells_html", 2048, """ - Truncate cells longer than this in HTML table view. Set to 0 to disable. + Truncate cells longer than this in HTML table view - set 0 to disable + """.strip()), + ConfigOption("force_https_urls", False, """ + Force URLs in API output to always use https:// protocol """.strip()), ) DEFAULT_CONFIG = { diff --git a/datasette/views/base.py b/datasette/views/base.py index 88f492f0..44381cb9 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -142,6 +142,12 @@ class BaseView(RenderMixin): return name, expected, None + def absolute_url(self, request, path): + url = urllib.parse.urljoin(request.url, path) + if url.startswith("http://") and self.ds.config["force_https_urls"]: + url = "https://" + url[len("http://"):] + return url + def get_templates(self, database, table=None): assert NotImplemented diff --git a/datasette/views/table.py b/datasette/views/table.py index 92c187ac..654e60fa 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -1,4 +1,3 @@ -from collections import namedtuple import sqlite3 import urllib @@ -564,9 +563,7 @@ class TableView(RowTableShared): row["value"] ), "count": row["count"], - "toggle_url": urllib.parse.urljoin( - request.url, toggle_path - ), + "toggle_url": self.absolute_url(request, toggle_path), "selected": selected, }) except InterruptedError: @@ -650,8 +647,8 @@ class TableView(RowTableShared): added_args["_sort_desc"] = sort_desc else: added_args = {"_next": next_value} - next_url = urllib.parse.urljoin( - request.url, path_with_replaced_args(request, added_args) + next_url = self.absolute_url( + request, path_with_replaced_args(request, added_args) ) rows = rows[:page_size] @@ -702,8 +699,10 @@ class TableView(RowTableShared): ): suggested_facets.append({ 'name': facet_column, - 'toggle_url': path_with_added_args( - request, {'_facet': facet_column} + 'toggle_url': self.absolute_url( + request, path_with_added_args( + request, {"_facet": facet_column} + ) ), }) except InterruptedError: diff --git a/docs/config.rst b/docs/config.rst index 81f68602..a6bb0b23 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -158,8 +158,22 @@ You can disable the limit entirely by settings this to 0: truncate_cells_html ------------------- -In the HTML table view, truncate any strings that are longer than this value. The full value will still be available in CSV, JSON and on the individual row HTML page. Set this to 0 to disable truncation. +In the HTML table view, truncate any strings that are longer than this value. +The full value will still be available in CSV, JSON and on the individual row +HTML page. Set this to 0 to disable truncation. :: datasette mydatabase.db --config truncate_cells_html:0 + + +force_https_urls +---------------- + +Forces self-referential URLs in the JSON output to always use the ``https://`` +protocol. This is useful for cases where the application itself is hosted using +HTTP but is served to the outside world via a proxy that enables HTTPS. + +:: + + datasette mydatabase.db --config force_https_urls:1 diff --git a/tests/test_api.py b/tests/test_api.py index 9e35697d..ac9fb615 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -975,6 +975,7 @@ def test_config_json(app_client): "allow_csv_stream": True, "max_csv_mb": 100, "truncate_cells_html": 2048, + "force_https_urls": False, } == response.json @@ -1270,3 +1271,13 @@ def test_config_cache_size(app_client_larger_cache_size): '/fixtures/pragma_cache_size.json' ) assert [[-2500]] == response.json['rows'] + + +def test_config_force_https_urls(): + for client in app_client(config={"force_https_urls": True}): + response = client.get("/fixtures/facetable.json?_size=3&_facet=state") + assert response.json["next_url"].startswith("https://") + assert response.json["facet_results"]["state"]["results"][0][ + "toggle_url" + ].startswith("https://") + assert response.json["suggested_facets"][0]["toggle_url"].startswith("https://")