diff --git a/whyis/static/js/whyis_vue/components/pages/vega/editor/vega-editor.vue b/whyis/static/js/whyis_vue/components/pages/vega/editor/vega-editor.vue
index b01b7d975..619a86717 100644
--- a/whyis/static/js/whyis_vue/components/pages/vega/editor/vega-editor.vue
+++ b/whyis/static/js/whyis_vue/components/pages/vega/editor/vega-editor.vue
@@ -1,3 +1,9 @@
+
From 278bea51ff03c53f5781bbdd6e684516e716c2e5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:43:27 +0000
Subject: [PATCH 04/26] Fix SPARQL proxy to preserve form data when forwarding
to Fuseki
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 25 ++++++++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 8336ecccb..5a12c8c93 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -36,13 +36,28 @@ def sparql_view():
elif request.method == 'POST':
if 'application/sparql-update' in request.headers.get('content-type', ''):
return "Update not allowed.", 403
+
+ # Get the raw data BEFORE accessing request.values
+ # because accessing request.values consumes the input stream
+ raw_data = request.get_data()
+
if 'update' in request.values:
return "Update not allowed.", 403
+ # Prepare headers for proxying - remove headers that should not be forwarded
+ headers = {}
+ headers.update(request.headers)
+ # Remove hop-by-hop headers and headers that will be set by requests library
+ for header in ['Host', 'Content-Length', 'Connection', 'Keep-Alive',
+ 'Proxy-Authenticate', 'Proxy-Authorization', 'TE', 'Trailers',
+ 'Transfer-Encoding', 'Upgrade']:
+ if header in headers:
+ del headers[header]
+
req = current_app.db.store.raw_sparql_request(
method='POST',
- headers=dict(request.headers),
- data=request.get_data()
+ headers=headers,
+ data=raw_data
)
except NotImplementedError as e:
# Local stores don't support proxying - return error
@@ -64,10 +79,14 @@ def sparql_view():
elif request.method == 'POST':
if 'application/sparql-update' in request.headers.get('content-type', ''):
return "Update not allowed.", 403
+
+ # Get raw data before accessing request.values
+ raw_data = request.get_data()
+
if 'update' in request.values:
return "Update not allowed.", 403
req = requests.post(current_app.db.store.query_endpoint,
- headers=request.headers, data=request.values, stream=True)
+ headers=request.headers, data=raw_data, stream=True)
# Return the response
response = Response(FileLikeFromIter(req.iter_content()),
From 2d6a0b2f5af97e0c2d51f155a87c41381c30409d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:44:09 +0000
Subject: [PATCH 05/26] Address code review feedback: improve header handling
consistency
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 5a12c8c93..f7afd446b 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -45,8 +45,7 @@ def sparql_view():
return "Update not allowed.", 403
# Prepare headers for proxying - remove headers that should not be forwarded
- headers = {}
- headers.update(request.headers)
+ headers = dict(request.headers)
# Remove hop-by-hop headers and headers that will be set by requests library
for header in ['Host', 'Content-Length', 'Connection', 'Keep-Alive',
'Proxy-Authenticate', 'Proxy-Authorization', 'TE', 'Trailers',
@@ -85,8 +84,17 @@ def sparql_view():
if 'update' in request.values:
return "Update not allowed.", 403
+
+ # Prepare headers for proxying - same filtering as main path
+ headers = dict(request.headers)
+ for header in ['Host', 'Content-Length', 'Connection', 'Keep-Alive',
+ 'Proxy-Authenticate', 'Proxy-Authorization', 'TE', 'Trailers',
+ 'Transfer-Encoding', 'Upgrade']:
+ if header in headers:
+ del headers[header]
+
req = requests.post(current_app.db.store.query_endpoint,
- headers=request.headers, data=raw_data, stream=True)
+ headers=headers, data=raw_data, stream=True)
# Return the response
response = Response(FileLikeFromIter(req.iter_content()),
From 0e3db5009b2ac5e26d58f43860ca615d43a6e8e3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:44:55 +0000
Subject: [PATCH 06/26] Extract hop-by-hop headers list to module constant for
maintainability
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index f7afd446b..b90891584 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -6,6 +6,15 @@
from setlr import FileLikeFromIter
+# HTTP headers that should not be forwarded when proxying requests
+# These are hop-by-hop headers or headers that will be set by the requests library
+HOP_BY_HOP_HEADERS = [
+ 'Host', 'Content-Length', 'Connection', 'Keep-Alive',
+ 'Proxy-Authenticate', 'Proxy-Authorization', 'TE', 'Trailers',
+ 'Transfer-Encoding', 'Upgrade'
+]
+
+
@sparql_blueprint.route('/sparql', methods=['GET', 'POST'])
@conditional_login_required
def sparql_view():
@@ -47,9 +56,7 @@ def sparql_view():
# Prepare headers for proxying - remove headers that should not be forwarded
headers = dict(request.headers)
# Remove hop-by-hop headers and headers that will be set by requests library
- for header in ['Host', 'Content-Length', 'Connection', 'Keep-Alive',
- 'Proxy-Authenticate', 'Proxy-Authorization', 'TE', 'Trailers',
- 'Transfer-Encoding', 'Upgrade']:
+ for header in HOP_BY_HOP_HEADERS:
if header in headers:
del headers[header]
@@ -87,9 +94,7 @@ def sparql_view():
# Prepare headers for proxying - same filtering as main path
headers = dict(request.headers)
- for header in ['Host', 'Content-Length', 'Connection', 'Keep-Alive',
- 'Proxy-Authenticate', 'Proxy-Authorization', 'TE', 'Trailers',
- 'Transfer-Encoding', 'Upgrade']:
+ for header in HOP_BY_HOP_HEADERS:
if header in headers:
del headers[header]
From 88481e3af127c18c86eb9dd9ec974b0ba23a1c78 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:45:44 +0000
Subject: [PATCH 07/26] Add case-insensitive header filtering helper function
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 35 ++++++++++++++++++---------
1 file changed, 24 insertions(+), 11 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index b90891584..85253fbde 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -15,6 +15,26 @@
]
+def filter_headers_for_proxying(headers):
+ """
+ Filter out hop-by-hop headers that should not be forwarded when proxying.
+
+ Args:
+ headers: Flask headers object or dict of headers
+
+ Returns:
+ dict: Filtered headers suitable for forwarding
+ """
+ filtered = dict(headers)
+ # Use case-insensitive comparison since HTTP headers are case-insensitive
+ headers_lower = {k.lower(): k for k in filtered.keys()}
+ for header in HOP_BY_HOP_HEADERS:
+ header_lower = header.lower()
+ if header_lower in headers_lower:
+ del filtered[headers_lower[header_lower]]
+ return filtered
+
+
@sparql_blueprint.route('/sparql', methods=['GET', 'POST'])
@conditional_login_required
def sparql_view():
@@ -53,12 +73,8 @@ def sparql_view():
if 'update' in request.values:
return "Update not allowed.", 403
- # Prepare headers for proxying - remove headers that should not be forwarded
- headers = dict(request.headers)
- # Remove hop-by-hop headers and headers that will be set by requests library
- for header in HOP_BY_HOP_HEADERS:
- if header in headers:
- del headers[header]
+ # Filter headers for proxying
+ headers = filter_headers_for_proxying(request.headers)
req = current_app.db.store.raw_sparql_request(
method='POST',
@@ -92,11 +108,8 @@ def sparql_view():
if 'update' in request.values:
return "Update not allowed.", 403
- # Prepare headers for proxying - same filtering as main path
- headers = dict(request.headers)
- for header in HOP_BY_HOP_HEADERS:
- if header in headers:
- del headers[header]
+ # Filter headers for proxying
+ headers = filter_headers_for_proxying(request.headers)
req = requests.post(current_app.db.store.query_endpoint,
headers=headers, data=raw_data, stream=True)
From 1f4e1b4afee1b5001ad6c3839bd7cbfd1134b5f1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:46:23 +0000
Subject: [PATCH 08/26] Improve documentation for filter_headers_for_proxying
function
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 85253fbde..828a15225 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -19,11 +19,14 @@ def filter_headers_for_proxying(headers):
"""
Filter out hop-by-hop headers that should not be forwarded when proxying.
+ Performs case-insensitive header matching to comply with HTTP standards,
+ which specify that header names are case-insensitive.
+
Args:
headers: Flask headers object or dict of headers
Returns:
- dict: Filtered headers suitable for forwarding
+ dict: Filtered headers suitable for forwarding (with hop-by-hop headers removed)
"""
filtered = dict(headers)
# Use case-insensitive comparison since HTTP headers are case-insensitive
From 57474bd11fb07575ff546a22ebcd5b3a3b5f72e9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:47:03 +0000
Subject: [PATCH 09/26] Add clarifying comments for request handling and
streaming behavior
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 828a15225..60e394c9c 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -70,7 +70,8 @@ def sparql_view():
return "Update not allowed.", 403
# Get the raw data BEFORE accessing request.values
- # because accessing request.values consumes the input stream
+ # because accessing request.values consumes the input stream,
+ # which would cause request.get_data() to return empty bytes
raw_data = request.get_data()
if 'update' in request.values:
@@ -84,6 +85,8 @@ def sparql_view():
headers=headers,
data=raw_data
)
+ # Note: raw_sparql_request returns a response object that supports
+ # iter_content() for streaming, same as requests library responses
except NotImplementedError as e:
# Local stores don't support proxying - return error
return str(e), 501
From 30ba360bf652ef37806d5446d3e3c3524eb4a757 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:47:44 +0000
Subject: [PATCH 10/26] Clarify comments about request.values access ordering
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 60e394c9c..e282a741b 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -69,9 +69,10 @@ def sparql_view():
if 'application/sparql-update' in request.headers.get('content-type', ''):
return "Update not allowed.", 403
- # Get the raw data BEFORE accessing request.values
- # because accessing request.values consumes the input stream,
- # which would cause request.get_data() to return empty bytes
+ # Get the raw data BEFORE accessing request.values.
+ # Flask's request.values consumes the input stream, making the body
+ # unavailable for get_data(). By calling get_data() first, we preserve
+ # the raw body, and can then safely access request.values.
raw_data = request.get_data()
if 'update' in request.values:
@@ -108,7 +109,7 @@ def sparql_view():
if 'application/sparql-update' in request.headers.get('content-type', ''):
return "Update not allowed.", 403
- # Get raw data before accessing request.values
+ # Get raw data before accessing request.values (see comment above in main path)
raw_data = request.get_data()
if 'update' in request.values:
From c97441036a65952ed56eea8af0a209f647ba9306 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:48:21 +0000
Subject: [PATCH 11/26] Add comment explaining why raw_data is used instead of
request.values
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index e282a741b..71f1e972b 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -118,6 +118,7 @@ def sparql_view():
# Filter headers for proxying
headers = filter_headers_for_proxying(request.headers)
+ # Send raw_data (bytes) not request.values (dict) to preserve exact form encoding
req = requests.post(current_app.db.store.query_endpoint,
headers=headers, data=raw_data, stream=True)
From ad6620d713c695c1020fe084d65083c2f6e07243 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:49:04 +0000
Subject: [PATCH 12/26] Optimize filter_headers_for_proxying with single-pass
dictionary comprehension
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 71f1e972b..3bbe954c1 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -28,14 +28,11 @@ def filter_headers_for_proxying(headers):
Returns:
dict: Filtered headers suitable for forwarding (with hop-by-hop headers removed)
"""
- filtered = dict(headers)
- # Use case-insensitive comparison since HTTP headers are case-insensitive
- headers_lower = {k.lower(): k for k in filtered.keys()}
- for header in HOP_BY_HOP_HEADERS:
- header_lower = header.lower()
- if header_lower in headers_lower:
- del filtered[headers_lower[header_lower]]
- return filtered
+ # Create set of lowercase header names to filter for efficient lookup
+ hop_by_hop_lower = {h.lower() for h in HOP_BY_HOP_HEADERS}
+
+ # Filter headers in a single pass with case-insensitive comparison
+ return {k: v for k, v in headers.items() if k.lower() not in hop_by_hop_lower}
@sparql_blueprint.route('/sparql', methods=['GET', 'POST'])
From db90f62192a289e34a3350ff8994364cb99ed116 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:49:36 +0000
Subject: [PATCH 13/26] Remove redundant comment about streaming behavior
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 3bbe954c1..875fa2b69 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -83,8 +83,6 @@ def sparql_view():
headers=headers,
data=raw_data
)
- # Note: raw_sparql_request returns a response object that supports
- # iter_content() for streaming, same as requests library responses
except NotImplementedError as e:
# Local stores don't support proxying - return error
return str(e), 501
From 9a017d44f4332066cba5ed36198984d2e5c025c6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:50:20 +0000
Subject: [PATCH 14/26] Optimize with module-level constant and improve comment
clarity
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 875fa2b69..b5813c381 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -14,6 +14,9 @@
'Transfer-Encoding', 'Upgrade'
]
+# Lowercase version for efficient case-insensitive lookups
+HOP_BY_HOP_HEADERS_LOWER = {h.lower() for h in HOP_BY_HOP_HEADERS}
+
def filter_headers_for_proxying(headers):
"""
@@ -28,11 +31,8 @@ def filter_headers_for_proxying(headers):
Returns:
dict: Filtered headers suitable for forwarding (with hop-by-hop headers removed)
"""
- # Create set of lowercase header names to filter for efficient lookup
- hop_by_hop_lower = {h.lower() for h in HOP_BY_HOP_HEADERS}
-
# Filter headers in a single pass with case-insensitive comparison
- return {k: v for k, v in headers.items() if k.lower() not in hop_by_hop_lower}
+ return {k: v for k, v in headers.items() if k.lower() not in HOP_BY_HOP_HEADERS_LOWER}
@sparql_blueprint.route('/sparql', methods=['GET', 'POST'])
@@ -104,7 +104,10 @@ def sparql_view():
if 'application/sparql-update' in request.headers.get('content-type', ''):
return "Update not allowed.", 403
- # Get raw data before accessing request.values (see comment above in main path)
+ # Get raw data before accessing request.values to preserve request body.
+ # Flask's request.values consumes the input stream, making the body
+ # unavailable for get_data(). By calling get_data() first, we preserve
+ # the raw body, and can then safely access request.values.
raw_data = request.get_data()
if 'update' in request.values:
From e44b19f21cd5581825e1c58dc67fb3c6ece5f070 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 17:50:53 +0000
Subject: [PATCH 15/26] Clarify architectural reason for dual proxying
approaches
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/blueprint/sparql/sparql_view.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index b5813c381..649a91c93 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -91,8 +91,9 @@ def sparql_view():
current_app.logger.error(f"SPARQL request failed: {str(e)}")
return f"SPARQL request failed: {str(e)}", 500
else:
- # Fallback for stores without raw_sparql_request (should not happen)
- # This is the old behavior - direct HTTP request without authentication
+ # Fallback for stores without raw_sparql_request (should not happen in practice)
+ # This path uses requests library directly without authentication support
+ # Modern stores should implement raw_sparql_request for proper auth handling
if request.method == 'GET':
headers = {}
headers.update(request.headers)
From 17b77c5a5928817436152359832e1ddc7a70e1d8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 18:20:51 +0000
Subject: [PATCH 16/26] Remove args configuration from YASQE to fix duplicate
query parameter
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/static/js/whyis_vue/components/yasqe.vue | 4 ----
1 file changed, 4 deletions(-)
diff --git a/whyis/static/js/whyis_vue/components/yasqe.vue b/whyis/static/js/whyis_vue/components/yasqe.vue
index 3a88b9074..28ae70f3f 100644
--- a/whyis/static/js/whyis_vue/components/yasqe.vue
+++ b/whyis/static/js/whyis_vue/components/yasqe.vue
@@ -37,10 +37,6 @@ export default Vue.component("yasqe", {
showQueryButton: !this.showBtns,
endpoint: this.endpoint,
requestMethod: "POST",
- // Ensure query parameter is sent in POST body
- args: [
- { name: "query", value: null }
- ],
callbacks: {
error() {
console.error("YASQE query error", arguments);
From 7d0ef71c1753ba557ac45959bb831e8cb3eeeacc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 18:29:55 +0000
Subject: [PATCH 17/26] Fix Vega-Lite URL construction error by removing
$schema before validation
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue b/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue
index ddef7c204..7c9f6793e 100644
--- a/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue
+++ b/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue
@@ -54,7 +54,12 @@ export default {
this.$emit('new-vega-view', result.view)
},
validateSpec () {
- const validation = jsonValidate(this.spec, vegaLiteSchema)
+ // Create a copy of the spec without $schema to avoid URL resolution issues
+ // The $schema property can cause jsonValidate to attempt URL construction
+ const specToValidate = Object.assign({}, this.spec)
+ delete specToValidate.$schema
+
+ const validation = jsonValidate(specToValidate, vegaLiteSchema)
if (!validation.valid) {
console.warn('Invalid spec', validation)
} else {
From efdc860d972c3369a7aa7e74dd12638e68432909 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 18:30:54 +0000
Subject: [PATCH 18/26] Add clarifying comment about shallow copy sufficiency
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue b/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue
index 7c9f6793e..e4158c32e 100644
--- a/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue
+++ b/whyis/static/js/whyis_vue/components/vega-lite-wrapper.vue
@@ -54,8 +54,10 @@ export default {
this.$emit('new-vega-view', result.view)
},
validateSpec () {
- // Create a copy of the spec without $schema to avoid URL resolution issues
- // The $schema property can cause jsonValidate to attempt URL construction
+ // Create a shallow copy of the spec to remove $schema property
+ // Shallow copy is sufficient since we only delete top-level $schema
+ // and don't modify nested properties. The $schema property can cause
+ // jsonValidate to attempt URL construction, which fails
const specToValidate = Object.assign({}, this.spec)
delete specToValidate.$schema
From c1392a6853a9d7d28c5b38d3d61dbba117304d10 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:00:38 +0000
Subject: [PATCH 19/26] Add CORS support to enable cross-origin API access
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
setup.py | 1 +
tests/api/test_cors.py | 50 ++++++++++++++++++++++++++++++++++++++++++
whyis/app.py | 14 ++++++++++++
3 files changed, 65 insertions(+)
create mode 100644 tests/api/test_cors.py
diff --git a/setup.py b/setup.py
index 607e8a3bc..627593f5f 100644
--- a/setup.py
+++ b/setup.py
@@ -203,6 +203,7 @@ def run(self):
'xlrd==2.0.1',
'werkzeug==2.0.3',
'Flask-Caching==1.10.1',
+ 'Flask-CORS>=3.0.10',
'pytz'
],
tests_require=[
diff --git a/tests/api/test_cors.py b/tests/api/test_cors.py
new file mode 100644
index 000000000..dadb17db2
--- /dev/null
+++ b/tests/api/test_cors.py
@@ -0,0 +1,50 @@
+"""
+Test CORS (Cross-Origin Resource Sharing) headers.
+
+Verifies that CORS headers are properly set for API endpoints.
+"""
+
+import pytest
+
+
+def test_cors_headers_on_root(client):
+ """Test that CORS headers are present on root endpoint."""
+ response = client.get('/')
+
+ # Check for CORS headers
+ assert 'Access-Control-Allow-Origin' in response.headers
+ assert response.headers['Access-Control-Allow-Origin'] == '*'
+
+
+def test_cors_preflight_request(client):
+ """Test CORS preflight (OPTIONS) request."""
+ response = client.options(
+ '/sparql',
+ headers={
+ 'Origin': 'http://example.com',
+ 'Access-Control-Request-Method': 'POST',
+ 'Access-Control-Request-Headers': 'Content-Type'
+ }
+ )
+
+ # Check CORS preflight response headers
+ assert 'Access-Control-Allow-Origin' in response.headers
+ assert 'Access-Control-Allow-Methods' in response.headers
+ assert 'Access-Control-Allow-Headers' in response.headers
+
+
+def test_cors_headers_on_sparql_endpoint(client):
+ """Test that CORS headers are present on SPARQL endpoint."""
+ response = client.get('/sparql')
+
+ # Check for CORS headers
+ assert 'Access-Control-Allow-Origin' in response.headers
+
+
+def test_cors_headers_on_api_endpoint(client):
+ """Test that CORS headers are present on API endpoints."""
+ # Test with the nanopub list endpoint
+ response = client.get('/pub/')
+
+ # Check for CORS headers
+ assert 'Access-Control-Allow-Origin' in response.headers
diff --git a/whyis/app.py b/whyis/app.py
index bc71c8152..e2826c8d3 100644
--- a/whyis/app.py
+++ b/whyis/app.py
@@ -22,6 +22,7 @@
from depot.middleware import FileServeApp
from depot.io.utils import FileIntent
from flask import render_template, g, redirect, url_for, request, flash, send_from_directory, abort
+from flask_cors import CORS
from flask_security import Security
from flask_security.core import current_user
from flask_security.forms import RegisterForm
@@ -97,6 +98,19 @@ def configure_extensions(self):
Empty.configure_extensions(self)
+ # Configure CORS to allow cross-origin requests from any origin
+ # This enables external applications to access Whyis APIs and data
+ CORS(self, resources={
+ r"/*": {
+ "origins": "*",
+ "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
+ "allow_headers": ["Content-Type", "Authorization", "Accept"],
+ "expose_headers": ["Content-Type", "Authorization"],
+ "supports_credentials": True,
+ "max_age": 3600
+ }
+ })
+
if self.config.get('EMBEDDED_CELERY',False):
# self.config['CELERY_BROKER_URL'] = 'redis://localhost:6379'
# self.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379'
From e38d1b3b01e081de5a7a1c9ad3a7eafc4c01a4df Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:01:24 +0000
Subject: [PATCH 20/26] Fix CORS security issue by disabling credentials with
wildcard origins
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/app.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/whyis/app.py b/whyis/app.py
index e2826c8d3..c4fa0ebbd 100644
--- a/whyis/app.py
+++ b/whyis/app.py
@@ -100,13 +100,14 @@ def configure_extensions(self):
# Configure CORS to allow cross-origin requests from any origin
# This enables external applications to access Whyis APIs and data
+ # Note: supports_credentials is False when using wildcard origins for security
CORS(self, resources={
r"/*": {
"origins": "*",
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
"allow_headers": ["Content-Type", "Authorization", "Accept"],
"expose_headers": ["Content-Type", "Authorization"],
- "supports_credentials": True,
+ "supports_credentials": False,
"max_age": 3600
}
})
From c46963dad9002429173bbf872c9fc85e960cc416 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:02:02 +0000
Subject: [PATCH 21/26] Extract CORS max_age to named constant for better
maintainability
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/app.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/whyis/app.py b/whyis/app.py
index c4fa0ebbd..7cd4c9276 100644
--- a/whyis/app.py
+++ b/whyis/app.py
@@ -101,6 +101,7 @@ def configure_extensions(self):
# Configure CORS to allow cross-origin requests from any origin
# This enables external applications to access Whyis APIs and data
# Note: supports_credentials is False when using wildcard origins for security
+ CORS_MAX_AGE = 3600 # Preflight cache duration in seconds (1 hour)
CORS(self, resources={
r"/*": {
"origins": "*",
@@ -108,7 +109,7 @@ def configure_extensions(self):
"allow_headers": ["Content-Type", "Authorization", "Accept"],
"expose_headers": ["Content-Type", "Authorization"],
"supports_credentials": False,
- "max_age": 3600
+ "max_age": CORS_MAX_AGE
}
})
From 40b01697683c5f1f1b9dc14d0573dbd8b6bfd74e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:02:43 +0000
Subject: [PATCH 22/26] Make CORS origins configurable via CORS_ORIGINS setting
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/app.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/whyis/app.py b/whyis/app.py
index 7cd4c9276..b9e5202a8 100644
--- a/whyis/app.py
+++ b/whyis/app.py
@@ -101,10 +101,15 @@ def configure_extensions(self):
# Configure CORS to allow cross-origin requests from any origin
# This enables external applications to access Whyis APIs and data
# Note: supports_credentials is False when using wildcard origins for security
+ # To restrict origins, set CORS_ORIGINS in config (comma-separated list)
CORS_MAX_AGE = 3600 # Preflight cache duration in seconds (1 hour)
+ cors_origins = self.config.get('CORS_ORIGINS', '*')
+ if cors_origins != '*' and ',' in cors_origins:
+ cors_origins = [origin.strip() for origin in cors_origins.split(',')]
+
CORS(self, resources={
r"/*": {
- "origins": "*",
+ "origins": cors_origins,
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
"allow_headers": ["Content-Type", "Authorization", "Accept"],
"expose_headers": ["Content-Type", "Authorization"],
From ddde85105dad3ceb442dcebc7270a7dfb4b9a48d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:03:28 +0000
Subject: [PATCH 23/26] Fix CORS origins parsing to handle single origin
correctly
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/app.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/whyis/app.py b/whyis/app.py
index b9e5202a8..37f5ab767 100644
--- a/whyis/app.py
+++ b/whyis/app.py
@@ -101,11 +101,18 @@ def configure_extensions(self):
# Configure CORS to allow cross-origin requests from any origin
# This enables external applications to access Whyis APIs and data
# Note: supports_credentials is False when using wildcard origins for security
- # To restrict origins, set CORS_ORIGINS in config (comma-separated list)
+ # To restrict origins, set CORS_ORIGINS in config (comma-separated list or single origin)
CORS_MAX_AGE = 3600 # Preflight cache duration in seconds (1 hour)
cors_origins = self.config.get('CORS_ORIGINS', '*')
- if cors_origins != '*' and ',' in cors_origins:
- cors_origins = [origin.strip() for origin in cors_origins.split(',')]
+
+ # Parse CORS_ORIGINS: wildcard, single origin, or comma-separated list
+ if cors_origins != '*':
+ if ',' in cors_origins:
+ # Multiple origins separated by commas
+ cors_origins = [origin.strip() for origin in cors_origins.split(',')]
+ else:
+ # Single origin - wrap in list for Flask-CORS
+ cors_origins = [cors_origins.strip()]
CORS(self, resources={
r"/*": {
From c005b12d1e8d1cfa8abf0290cb56c8b99ff8d498 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:04:40 +0000
Subject: [PATCH 24/26] Make CORS credentials configurable and enhance test
coverage
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
tests/api/test_cors.py | 30 +++++++++++++++++++++++++++++-
whyis/app.py | 12 ++++++++++--
2 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/tests/api/test_cors.py b/tests/api/test_cors.py
index dadb17db2..6d2420ac1 100644
--- a/tests/api/test_cors.py
+++ b/tests/api/test_cors.py
@@ -17,7 +17,7 @@ def test_cors_headers_on_root(client):
def test_cors_preflight_request(client):
- """Test CORS preflight (OPTIONS) request."""
+ """Test CORS preflight (OPTIONS) request with detailed header validation."""
response = client.options(
'/sparql',
headers={
@@ -31,6 +31,18 @@ def test_cors_preflight_request(client):
assert 'Access-Control-Allow-Origin' in response.headers
assert 'Access-Control-Allow-Methods' in response.headers
assert 'Access-Control-Allow-Headers' in response.headers
+
+ # Validate allowed methods include expected values
+ allowed_methods = response.headers.get('Access-Control-Allow-Methods', '')
+ expected_methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']
+ for method in expected_methods:
+ assert method in allowed_methods, f"Expected method {method} not in allowed methods"
+
+ # Validate allowed headers include expected values
+ allowed_headers = response.headers.get('Access-Control-Allow-Headers', '').lower()
+ expected_headers = ['content-type', 'authorization', 'accept']
+ for header in expected_headers:
+ assert header in allowed_headers, f"Expected header {header} not in allowed headers"
def test_cors_headers_on_sparql_endpoint(client):
@@ -48,3 +60,19 @@ def test_cors_headers_on_api_endpoint(client):
# Check for CORS headers
assert 'Access-Control-Allow-Origin' in response.headers
+
+
+def test_cors_max_age_header(client):
+ """Test that CORS max age header is set correctly."""
+ response = client.options(
+ '/sparql',
+ headers={
+ 'Origin': 'http://example.com',
+ 'Access-Control-Request-Method': 'GET'
+ }
+ )
+
+ # Check for max age header
+ assert 'Access-Control-Max-Age' in response.headers
+ # Verify it's set to 3600 (1 hour)
+ assert response.headers['Access-Control-Max-Age'] == '3600'
diff --git a/whyis/app.py b/whyis/app.py
index 37f5ab767..7747801a6 100644
--- a/whyis/app.py
+++ b/whyis/app.py
@@ -100,8 +100,9 @@ def configure_extensions(self):
# Configure CORS to allow cross-origin requests from any origin
# This enables external applications to access Whyis APIs and data
- # Note: supports_credentials is False when using wildcard origins for security
+ # Note: supports_credentials should be False with wildcard origins for security
# To restrict origins, set CORS_ORIGINS in config (comma-separated list or single origin)
+ # To enable credentials, set CORS_SUPPORTS_CREDENTIALS=True (only with specific origins)
CORS_MAX_AGE = 3600 # Preflight cache duration in seconds (1 hour)
cors_origins = self.config.get('CORS_ORIGINS', '*')
@@ -114,13 +115,20 @@ def configure_extensions(self):
# Single origin - wrap in list for Flask-CORS
cors_origins = [cors_origins.strip()]
+ # supports_credentials can only be True with specific origins (not wildcard)
+ supports_credentials = self.config.get('CORS_SUPPORTS_CREDENTIALS', False)
+ if supports_credentials and cors_origins == '*':
+ # Warn and disable credentials if wildcard is used
+ print("WARNING: CORS_SUPPORTS_CREDENTIALS cannot be True with wildcard origins. Disabling.")
+ supports_credentials = False
+
CORS(self, resources={
r"/*": {
"origins": cors_origins,
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
"allow_headers": ["Content-Type", "Authorization", "Accept"],
"expose_headers": ["Content-Type", "Authorization"],
- "supports_credentials": False,
+ "supports_credentials": supports_credentials,
"max_age": CORS_MAX_AGE
}
})
From 36fa4cd86ca690616caec5279a347532df850ac3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Feb 2026 19:05:27 +0000
Subject: [PATCH 25/26] Make CORS max_age configurable and use proper logging
Co-authored-by: jpmccu <602385+jpmccu@users.noreply.github.com>
---
whyis/app.py | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/whyis/app.py b/whyis/app.py
index 7747801a6..7a707344e 100644
--- a/whyis/app.py
+++ b/whyis/app.py
@@ -101,9 +101,11 @@ def configure_extensions(self):
# Configure CORS to allow cross-origin requests from any origin
# This enables external applications to access Whyis APIs and data
# Note: supports_credentials should be False with wildcard origins for security
- # To restrict origins, set CORS_ORIGINS in config (comma-separated list or single origin)
- # To enable credentials, set CORS_SUPPORTS_CREDENTIALS=True (only with specific origins)
- CORS_MAX_AGE = 3600 # Preflight cache duration in seconds (1 hour)
+ # Configuration options:
+ # CORS_ORIGINS: "*" (default), single origin, or comma-separated list
+ # CORS_SUPPORTS_CREDENTIALS: False (default), True (only with specific origins)
+ # CORS_MAX_AGE: 3600 (default), preflight cache duration in seconds
+ cors_max_age = self.config.get('CORS_MAX_AGE', 3600)
cors_origins = self.config.get('CORS_ORIGINS', '*')
# Parse CORS_ORIGINS: wildcard, single origin, or comma-separated list
@@ -119,7 +121,7 @@ def configure_extensions(self):
supports_credentials = self.config.get('CORS_SUPPORTS_CREDENTIALS', False)
if supports_credentials and cors_origins == '*':
# Warn and disable credentials if wildcard is used
- print("WARNING: CORS_SUPPORTS_CREDENTIALS cannot be True with wildcard origins. Disabling.")
+ self.logger.warning("CORS: CORS_SUPPORTS_CREDENTIALS cannot be True with wildcard origins. Disabling credentials.")
supports_credentials = False
CORS(self, resources={
@@ -129,7 +131,7 @@ def configure_extensions(self):
"allow_headers": ["Content-Type", "Authorization", "Accept"],
"expose_headers": ["Content-Type", "Authorization"],
"supports_credentials": supports_credentials,
- "max_age": CORS_MAX_AGE
+ "max_age": cors_max_age
}
})
From 18b47c11e2bc69aec69eae40f575d82ba5e3234c Mon Sep 17 00:00:00 2001
From: Jamie McCusker
Date: Wed, 4 Feb 2026 21:41:23 +0000
Subject: [PATCH 26/26] Fixed additional layout issues.
---
whyis/blueprint/sparql/sparql_view.py | 3 +-
whyis/filters.py | 6 +
.../components/pages/search/search-page.vue | 2 +-
.../pages/vega/editor/vega-editor.html | 198 ++++++++++--------
.../components/search-autocomplete.vue | 2 +-
.../components/vega-lite-wrapper.vue | 12 +-
.../js/whyis_vue/utilities/vega-chart.js | 4 +-
whyis/static/vite.config.js | 15 +-
whyis/templates/base_vue.html | 1 -
whyis/templates/class_view.html | 145 +++++++------
whyis/templates/elements/upload.html | 4 +-
whyis/templates/resource_view.html | 147 +++++++------
12 files changed, 303 insertions(+), 236 deletions(-)
diff --git a/whyis/blueprint/sparql/sparql_view.py b/whyis/blueprint/sparql/sparql_view.py
index 649a91c93..ee1587ded 100644
--- a/whyis/blueprint/sparql/sparql_view.py
+++ b/whyis/blueprint/sparql/sparql_view.py
@@ -77,7 +77,7 @@ def sparql_view():
# Filter headers for proxying
headers = filter_headers_for_proxying(request.headers)
-
+ print (raw_data)
req = current_app.db.store.raw_sparql_request(
method='POST',
headers=headers,
@@ -117,6 +117,7 @@ def sparql_view():
# Filter headers for proxying
headers = filter_headers_for_proxying(request.headers)
+ print(raw_data)
# Send raw_data (bytes) not request.values (dict) to preserve exact form encoding
req = requests.post(current_app.db.store.query_endpoint,
headers=headers, data=raw_data, stream=True)
diff --git a/whyis/filters.py b/whyis/filters.py
index 695966ff1..e9902c86d 100644
--- a/whyis/filters.py
+++ b/whyis/filters.py
@@ -59,6 +59,12 @@ def labelize(entry, key='about', label_key='label', fetch=False):
entry[label_key] = app.get_label(resource)
return entry
+ @app.template_filter('label')
+ def label(entity):
+ key_uri = rdflib.URIRef(entity)
+ resource = app.db.resource(key_uri)
+ return app.get_label(resource)
+
@app.template_filter('iter_labelize')
def iter_labelize(entries, *args, **kw):
for entry in entries:
diff --git a/whyis/static/js/whyis_vue/components/pages/search/search-page.vue b/whyis/static/js/whyis_vue/components/pages/search/search-page.vue
index 3af583ea2..3be80ef29 100644
--- a/whyis/static/js/whyis_vue/components/pages/search/search-page.vue
+++ b/whyis/static/js/whyis_vue/components/pages/search/search-page.vue
@@ -11,7 +11,7 @@
v-model="searchQuery"
type="text"
class="form-control"
- placeholder="Search knowledge base..."
+ placeholder="Search..."
@keydown.enter="performSearch"
/>