diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28e0d9e7..b3bc1f3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+
- Updated onto-deside configuration 2025-04-09 (#202).
+- In the configuration file: per query `httpProxies` setting replaces global `httpProxy` setting (#4, #47).
## [1.7.0] - 2025-04-09
diff --git a/README.md b/README.md
index f68dc268..ae5e3556 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Table of contents:
* [Logging in](#logging-in)
* [Configuration file](#configuration-file)
* [Specifying sources](#specifying-sources)
- * [About httpProxy](#about-httpproxy)
+ * [About httpProxies](#about-httpproxies)
* [Adding variable type](#adding-variable-type)
* [Templated queries](#templated-queries)
* [Templated queries with fixed values for the template variables](#templated-queries-with-fixed-values-for-the-template-variables)
@@ -147,7 +147,6 @@ The configuration file must follow the structure shown below.
"footer": "HTML components or text that will function as the footer (will be placed in the footer div.)",
"defaultIDP": "The default value used for IDP when logging in, this IDP can be manually changed in the Web app as well. ",
"queryFolder": "The base location of the queries, all query locations will start from this folder (relative to public folder).",
- "httpProxy": "Optional http proxy through which the requests will be rerouted - see documentation below.",
"introductionText": "The text that the app shows on the dashboard, which the app also shows when you first open it.",
"queryGroups" : [
{
@@ -166,7 +165,6 @@ The configuration file must follow the structure shown below.
"icon": "The key to the icon for the query. This is optional and a default menu icon will be used when left empty.",
"comunicaContext": {
"sources": "Initial array of sources over which the query should be executed",
- "useProxy": "true or false, whether the query should be executed through the proxy or not. This field is optional and defaults to false.",
... any other field that can be used in the Comunica query engine https://comunica.dev/docs/query/advanced/context/
},
"sourcesIndex": {
@@ -184,7 +182,13 @@ The configuration file must follow the structure shown below.
...
]
},
-
+ "httpProxies": [
+ {
+ "urlStart": "all sources whose url start with this string will be rerouted",
+ "httpProxy": "http proxy through which these sources will be rerouted - see also documentation 'About httpProxies' below."
+ }
+ ...
+ ],
"askQuery": {
"trueText": "The text that is to be shown when the query result is true (in ASK queries).",
"falseText": "The text that is to be shown when the query result is false (in ASK queries)."
@@ -209,15 +213,22 @@ The (auxiliary) query provided in `sourceIndex.queryLocation` is executed on `so
If `sourceIndex` is used and there is no `comunicaContext.lenient` property found, one will be created with value `true`.
This makes sure that the (main) query can succeed if not all obtained sources are accessible.
-### About httpProxy
+### About httpProxies
+
+Per query, an optional array of `httpProxies` can be specified.
+An http proxy can be used to solve CORS issues in case CORS headers are not set (correctly) on some queried sources.
+Note that the involved sources can include those specified in `comunicaContext.sources` as well as those described in and found through `sourceIndex`.
-Configuration setting `httpProxy` can be used to solve CORS issues in case CORS headers are not set (correctly) on a queried source.
We support static proxies such as [cors-anywhere](https://www.npmjs.com/package/cors-anywhere) that take the URL from the path.
-We simply prepend the `httpProxy` value before the URL of each source in a query that has `comunicaContext.useProxy` set to `true`.
+Each element of such array contains a property `httpProxies` and a property `urlStart`.
+
+We simply prepend the `httpProxy` value before the URL of each source whose URL starts with the string in the corresponding `urlStart` value.
-Example: if `httpProxy` is set to `http://myproxy.org/`, source `http://www.example.com/source-xyz`
-will be accessed as `http://myproxy.org/http://www.example.com/source-xyz`.
+Example: if
+`httpProxies[i].urlStart` is set to `http://www.example.com/path-xyz` and
+`httpProxies[i].httpProxy` is set to `http://myproxy.org/`,
+source `http://www.example.com/path-xyz-source-xyz` will be accessed as `http://myproxy.org/http://www.example.com/path-xyz-source-xyz`.
### Adding variable type
diff --git a/main/configs/demo/config.json b/main/configs/demo/config.json
index 5264b633..a36f3b0a 100644
--- a/main/configs/demo/config.json
+++ b/main/configs/demo/config.json
@@ -7,7 +7,6 @@
"titleColor": "black",
"textColor": "#1976D2",
"queryFolder": "queries",
- "httpProxy": "http://localhost:8000/",
"showMilliseconds": false,
"defaultIDP": "http://localhost:8080",
"footer": "
IDLab - imec - UGent
",
@@ -33,9 +32,14 @@
"comunicaContext": {
"sources": [
"http://localhost:8001/example/idols"
- ],
- "useProxy": true
- }
+ ]
+ },
+ "httpProxies": [
+ {
+ "urlStart": "http://localhost:8001",
+ "httpProxy": "http://localhost:8000/"
+ }
+ ]
},
{
"id": "1010",
diff --git a/main/configs/test/config.json b/main/configs/test/config.json
index a5767c4c..be6270d7 100644
--- a/main/configs/test/config.json
+++ b/main/configs/test/config.json
@@ -7,7 +7,6 @@
"titleColor": "black",
"textColor": "#1976D2",
"queryFolder": "queries",
- "httpProxy": "http://localhost:8000/",
"showMilliseconds": false,
"defaultIDP": "http://localhost:8080",
"footer": "IDLab - imec - UGent
",
@@ -38,9 +37,14 @@
"comunicaContext": {
"sources": [
"http://localhost:8001/example/idols"
- ],
- "useProxy": true
- }
+ ]
+ },
+ "httpProxies": [
+ {
+ "urlStart": "http://localhost:8001",
+ "httpProxy": "http://localhost:8000/"
+ }
+ ]
},
{
"id": "1010",
@@ -504,6 +508,23 @@
],
"lenient": true
}
+ },
+ {
+ "id": "9130",
+ "queryGroupId": "gr-test",
+ "queryLocation": "components.rq",
+ "name": "Http proxy test combined with indirect sources and source verification",
+ "description": "Contents same as those for the 'Source verification' query, but now through http proxy in a worse case test.",
+ "sourcesIndex": {
+ "url": "http://localhost:8080/example/index-example-for-proxy-test-only",
+ "queryLocation": "/sourceQueries/index_example_common_lt.rq"
+ },
+ "httpProxies": [
+ {
+ "urlStart": "http://localhost:8080",
+ "httpProxy": "http://localhost:8000/"
+ }
+ ]
}
]
}
\ No newline at end of file
diff --git a/main/src/components/ActionBar/ActionBar.jsx b/main/src/components/ActionBar/ActionBar.jsx
index 3788db03..d2129f9b 100644
--- a/main/src/components/ActionBar/ActionBar.jsx
+++ b/main/src/components/ActionBar/ActionBar.jsx
@@ -106,10 +106,10 @@ function ActionBar() {
-
+
-
+
))}
diff --git a/main/src/components/ActionBar/SourceFetchStatusIcon/SourceFetchStatusIcon.jsx b/main/src/components/ActionBar/SourceFetchStatusIcon/SourceFetchStatusIcon.jsx
index a793ccf2..1cfd3786 100644
--- a/main/src/components/ActionBar/SourceFetchStatusIcon/SourceFetchStatusIcon.jsx
+++ b/main/src/components/ActionBar/SourceFetchStatusIcon/SourceFetchStatusIcon.jsx
@@ -8,19 +8,13 @@ import comunicaEngineWrapper from '../../../comunicaEngineWrapper/comunicaEngine
/**
* @param {object} props - the props passed to the component
- * @param {object} props.context - the query context
* @param {string} props.source - the source to check
- * @param {string} props.proxyUrl - the proxy url to use if the resource is accessed through a proxy
* @returns {Component} an icon indicating whether the query was executed succesfully or not
*/
-function SourceFetchStatusIcon({ context, source, proxyUrl }) {
- let actualSource = source;
- if (context.useProxy) {
- actualSource = `${proxyUrl}${source}`;
- }
- const status = comunicaEngineWrapper.getFetchStatusNumber(actualSource);
+function SourceFetchStatusIcon({ source }) {
+ const status = comunicaEngineWrapper.getFetchStatusNumber(source);
- if (comunicaEngineWrapper.getFetchSuccess(actualSource)) {
+ if (comunicaEngineWrapper.getFetchSuccess(source)) {
return (
@@ -44,9 +38,7 @@ function SourceFetchStatusIcon({ context, source, proxyUrl }) {
}
SourceFetchStatusIcon.propTypes = {
- context: PropTypes.object.isRequired,
- source: PropTypes.string.isRequired,
- proxyUrl: PropTypes.string.isRequired,
+ source: PropTypes.string.isRequired
};
export default SourceFetchStatusIcon;
diff --git a/main/src/components/ActionBar/SourceVerificationIcon/SourceVerificationIcon.jsx b/main/src/components/ActionBar/SourceVerificationIcon/SourceVerificationIcon.jsx
index 44d20e93..dfea34ba 100644
--- a/main/src/components/ActionBar/SourceVerificationIcon/SourceVerificationIcon.jsx
+++ b/main/src/components/ActionBar/SourceVerificationIcon/SourceVerificationIcon.jsx
@@ -7,6 +7,7 @@ import GppBadIcon from '@mui/icons-material/GppBad';
import GppMaybeIcon from '@mui/icons-material/GppMaybe';
import { coreVerify } from '../../../vendor/vcCore';
import comunicaEngineWrapper from '../../../comunicaEngineWrapper/comunicaEngineWrapper';
+import { translateUrlToProxiedUrl } from '../../../lib/utils';
const VERIFICATION_STATES = {
VERIFIED: 'VERIFIED',
@@ -17,16 +18,12 @@ const VERIFICATION_STATES = {
/**
* @param {object} props - the props passed to the component
- * @param {object} props.context - the query context
* @param {string} props.source - the source to check
- * @param {string} props.proxyUrl - the proxy url to use if the resource is accessed through a proxy
+ * @param {array} props.httpProxies - array of httpProxy definitions
* @returns {Component} an icon indicating whether the source was verified or not
*/
-function SourceVerificationIcon({ context, source, proxyUrl }) {
- let sourceUrl = source;
- if (context.useProxy) {
- sourceUrl = `${proxyUrl}${source}`;
- }
+function SourceVerificationIcon({ source, httpProxies }) {
+ let sourceUrl = translateUrlToProxiedUrl(source, httpProxies);
const [isLoading, setIsLoading] = useState(true);
const [verificationState, setVerificationState] = useState(undefined);
@@ -108,9 +105,8 @@ function SourceVerificationIcon({ context, source, proxyUrl }) {
}
SourceVerificationIcon.propTypes = {
- context: PropTypes.object.isRequired,
source: PropTypes.string.isRequired,
- proxyUrl: PropTypes.string.isRequired,
+ httpProxies: PropTypes.array.isRequired,
}
export default SourceVerificationIcon;
\ No newline at end of file
diff --git a/main/src/components/CustomQueryEditor/customConversionButton.jsx b/main/src/components/CustomQueryEditor/customConversionButton.jsx
index e06b0267..1c69c303 100644
--- a/main/src/components/CustomQueryEditor/customConversionButton.jsx
+++ b/main/src/components/CustomQueryEditor/customConversionButton.jsx
@@ -26,6 +26,7 @@ export default function CustomConversionButton({ id }) {
convertTemplatedQueriesFixedVariables()
convertComunicaContextAndSources()
convertASKquery()
+ convertHttpProxies()
// The id and group have to be deleted because a new custom query is going to be made. Name is modified.
delete convertedQuery.id
@@ -135,6 +136,13 @@ export default function CustomConversionButton({ id }) {
}
}
+ // This function handles the logic for http proxies
+ function convertHttpProxies() {
+ if (convertedQuery.httpProxies) {
+ convertedQuery.httpProxiesCheck = "on"
+ }
+ }
+
return (
diff --git a/main/src/components/CustomQueryEditor/customEditor.jsx b/main/src/components/CustomQueryEditor/customEditor.jsx
index 7563bbe2..a6c1185b 100644
--- a/main/src/components/CustomQueryEditor/customEditor.jsx
+++ b/main/src/components/CustomQueryEditor/customEditor.jsx
@@ -29,6 +29,7 @@ export default function CustomEditor(props) {
comunicaContextCheck: false,
sourceIndexCheck: false,
askQueryCheck: false,
+ httpProxiesCheck: false,
directVariablesCheck: false,
indirectVariablesCheck: false,
});
@@ -37,6 +38,7 @@ export default function CustomEditor(props) {
const [editError, setEditError] = useState(false);
const [parsingErrorComunica, setParsingErrorComunica] = useState(false);
const [parsingErrorAsk, setParsingErrorAsk] = useState(false);
+ const [parsingErrorHttpProxies, setParsingErrorHttpProxies] = useState(false);
const [parsingErrorTemplate, setParsingErrorTemplate] = useState(false);
// Default placeholders for the forms
@@ -61,6 +63,7 @@ ORDER BY ?genre`;
const defaultExtraComunicaContext = JSON.stringify({ "lenient": true }, null, 2);
const defaultAskQueryDetails = JSON.stringify({ "trueText": "this displays when true.", "falseText": "this displays when false." }, null, 2);
+ const defaultHttpProxiesDetails = JSON.stringify([{ "urlStart": "http://www.example.com/path-xyz", "httpProxy": "http://myproxy.org/" }], null, 2);
const defaultTemplateOptions = JSON.stringify(
{
"variableOne": [
@@ -100,7 +103,7 @@ ORDER BY ?genre`;
const handleSubmit = async (event) => {
event.preventDefault();
- if (!parsingErrorComunica && !parsingErrorAsk && !parsingErrorTemplate) {
+ if (!parsingErrorComunica && !parsingErrorAsk && !parsingErrorHttpProxies && !parsingErrorTemplate) {
setShowError(false);
const formData = new FormData(event.currentTarget);
const jsonData = Object.fromEntries(formData.entries());
@@ -190,6 +193,10 @@ ORDER BY ?genre`;
parsedObject.askQuery = JSON.parse(dataWithStrings.askQuery);
}
+ if (ensureBoolean(dataWithStrings.httpProxiesCheck)) {
+ parsedObject.httpProxies = JSON.parse(dataWithStrings.httpProxies);
+ }
+
if (ensureBoolean(dataWithStrings.directVariablesCheck)) {
parsedObject.variables = JSON.parse(dataWithStrings.variables);
}
@@ -545,6 +552,42 @@ ORDER BY ?genre`;
}
+ {
+ setParsingErrorHttpProxies(false);
+ setFormData((prevFormData) => ({
+ ...prevFormData,
+ 'httpProxiesCheck': !formData.httpProxiesCheck,
+ }));
+ }
+ }
+ />} label="Http proxies" />
+
+ {formData.httpProxiesCheck &&
+
+ handleJSONparsing(e, setParsingErrorHttpProxies)}
+ onChange={(e) => handleJSONparsing(e, setParsingErrorHttpProxies)}
+ sx={{ marginBottom: '16px' }}
+ />
+
+ }
+
diff --git a/main/src/comunicaEngineWrapper/comunicaEngineWrapper.js b/main/src/comunicaEngineWrapper/comunicaEngineWrapper.js
index 7a6fa6f5..dacedffa 100644
--- a/main/src/comunicaEngineWrapper/comunicaEngineWrapper.js
+++ b/main/src/comunicaEngineWrapper/comunicaEngineWrapper.js
@@ -4,6 +4,9 @@ import {
getDefaultSession,
fetch as authFetch,
} from "@inrupt/solid-client-authn-browser";
+import { translateUrlToProxiedUrl } from '../lib/utils';
+
+// LOG let wrappedFetchFunctionCounter = 0;
/**
* A class wrapping the Comunica engines we need, used for all but login actions.
@@ -58,12 +61,13 @@ class ComunicaEngineWrapper {
*
* @param {string} queryText - the SPARQL query text
* @param {object} context - the context to provide to the Comunica engine
- * @param {object} callbacks - an object contains the callback functions you specify
+ * @param {array} httpProxies - array of httpProxy definitions
+ * @param {object} callbacks - an object containing the callback functions you specify
* @returns {void} when the query has finished
*/
- async query(queryText, context, callbacks) {
+ async query(queryText, context, httpProxies, callbacks) {
try {
- this._prepareQuery(context);
+ this._prepareQuery(context, httpProxies);
let result = await this._engine.query(queryText, context);
switch (result.resultType) {
case 'bindings':
@@ -118,15 +122,16 @@ class ComunicaEngineWrapper {
* Supports the following options.
* - engine:
* - "default": use the default Comunica engine (this is the default anyway)
- * - "datasources": use a Comunica query engine configured to discover datasources recursively
+ * - "link-traversal": use a Comunica query engine configured to discover datasources recursively
*
* @param {string} queryText - the SPARQL SELECT query text
* @param {object} context - the context to provide to the Comunica engine
+ * @param {array} httpProxies - array of httpProxy definitions
* @param {string} options.engine - "default": use the default Comunica engine (this is the default anyway)
* - "link-traversal": use a Comunica query engine with link-traversal feature
* @returns {Promise } Promise to the bindings stream
*/
- async queryBindings(queryText, context, options = {}) {
+ async queryBindings(queryText, context, httpProxies, options = {}) {
let engine;
switch (options.engine) {
case "default":
@@ -140,7 +145,7 @@ class ComunicaEngineWrapper {
throw new Error("Unsupported engine requested");
}
try {
- this._prepareQuery(context);
+ this._prepareQuery(context, httpProxies);
return engine.queryBindings(queryText, context);
} catch (error) {
await this.reset();
@@ -152,8 +157,9 @@ class ComunicaEngineWrapper {
* Prepares a call to any engine's query function
*
* @param {object} context - the context that will be used
+ * @param {array} httpProxies - array of httpProxy definitions
*/
- _prepareQuery(context) {
+ _prepareQuery(context, httpProxies) {
// avoid faulty fetch status for sources cached in Comunica
for (const source of context.sources) {
this._fetchSuccess[source] = true;
@@ -162,29 +168,37 @@ class ComunicaEngineWrapper {
if (getDefaultSession().info.isLoggedIn) {
this._underlyingFetchFunction = authFetch;
}
- context.fetch = ComunicaEngineWrapper._getWrappedFetchFunction(this._underlyingFetchFunction, this);
+ context.fetch = ComunicaEngineWrapper._getWrappedFetchFunction(this._underlyingFetchFunction, httpProxies, this);
}
/**
* Returns a function that wraps the underlying fetch function and sets the fetch success information in member variables of _this.
*
- * @param underlyingFetchFunction - the underlying fetch functin
+ * @param underlyingFetchFunction - the underlying fetch function
+ * @param {array} httpProxies - array of httpProxy definitions
* @param {ComunicaEngineWrapper} _this - the calling ComunicaEngineWrapper object
* @returns {Function} that function.
*/
- static _getWrappedFetchFunction(underlyingFetchFunction, _this) {
+ static _getWrappedFetchFunction(underlyingFetchFunction, httpProxies, _this) {
const wrappedFetchFunction = async (arg) => {
try {
- const response = await underlyingFetchFunction(arg, {
+ let actualUrl = translateUrlToProxiedUrl(arg, httpProxies);
+ const response = await underlyingFetchFunction(actualUrl, {
headers: {
Accept: "application/n-quads,application/trig;q=0.9,text/turtle;q=0.8,application/n-triples;q=0.7,*/*;q=0.1"
}
});
_this._fetchSuccess[arg] = response.ok;
_this._fetchStatusNumber[arg] = response.status;
+ // LOG console.log(`--- wrappedFetchFunction #${++wrappedFetchFunctionCounter}`);
+ // LOG console.log(`arg: ${arg}`);
+ // LOG console.log(`actualUrl: ${actualUrl}`);
+ // LOG console.log(`response status: ${response.status}`);
return response;
}
catch (error) {
+ // LOG console.log(`--- wrappedFetchFunction #${++wrappedFetchFunctionCounter}`);
+ // LOG console.log(error);
_this._fetchSuccess[arg] = false;
throw error;
}
diff --git a/main/src/dataProvider/SparqlDataProvider.js b/main/src/dataProvider/SparqlDataProvider.js
index 272cec16..857e8553 100644
--- a/main/src/dataProvider/SparqlDataProvider.js
+++ b/main/src/dataProvider/SparqlDataProvider.js
@@ -1,4 +1,3 @@
-import { ProxyHandlerStatic } from "@comunica/actor-http-proxy";
import { Generator, Parser } from "sparqljs";
import NotImplementedError from "../NotImplementedError";
import { Term } from "sparqljs";
@@ -8,24 +7,6 @@ import comunicaEngineWrapper from "../comunicaEngineWrapper/comunicaEngineWrappe
let config = configManager.getConfig();
-let proxyHandler;
-
-const setProxyHandler = () => {
- if (config.httpProxy) {
- proxyHandler = new ProxyHandlerStatic(config.httpProxy);
- } else {
- proxyHandler = undefined;
- }
-};
-setProxyHandler();
-
-const onConfigChanged = (newConfig) => {
- config = newConfig;
- setProxyHandler();
-};
-
-configManager.on('configChanged', onConfigChanged);
-
// simple cache to save time while scrolling through pages of a list
// results = the result of executeQuery, totalItems
const listCache = {
@@ -57,7 +38,7 @@ export default {
handleComunicaContextCreation(query);
if (query.sourcesIndex) {
- const additionalSources = await getSourcesFromSourcesIndex(query.sourcesIndex, query.comunicaContext.useProxy);
+ const additionalSources = await getSourcesFromSourcesIndex(query.sourcesIndex, query.httpProxies);
query.comunicaContext.sources = [...new Set([...query.comunicaContext.sources, ...additionalSources])];
}
@@ -257,6 +238,7 @@ async function executeQuery(query) {
await comunicaEngineWrapper.query(query.queryText,
// WEIRD: we need to make a copy of the context here (a shallow copy is fine); concurrent calls ???
{ ...query.comunicaContext },
+ query.httpProxies && [ ...query.httpProxies ],
{ "variables": callbackVariables, "bindings": callbackBindings, "quads": callbackQuads, "boolean": callbackBoolean });
return results;
} catch (error) {
@@ -268,10 +250,10 @@ async function executeQuery(query) {
* Gets sources from a sources index
*
* @param {object} sourcesIndex - the sourcesIndex object as found in the configuration
- * @param {boolean} useProxy - true if the main query needs a proxy (in which case we implicitly use it to access the sources index too)
+ * @param {array} httpProxies - array of httpProxy definitions
* @returns {array} array of sources found
*/
-async function getSourcesFromSourcesIndex(sourcesIndex, useProxy) {
+async function getSourcesFromSourcesIndex(sourcesIndex, httpProxies) {
const sourcesList = [];
try {
let queryStringIndexSource;
@@ -283,7 +265,7 @@ async function getSourcesFromSourcesIndex(sourcesIndex, useProxy) {
}
const bindingsStream = await comunicaEngineWrapper.queryBindings(queryStringIndexSource,
- { lenient: true, sources: [sourcesIndex.url], httpProxyHandler: (useProxy ? proxyHandler : undefined) }, { engine: "link-traversal" });
+ { lenient: true, sources: [sourcesIndex.url] }, httpProxies, { engine: "link-traversal" });
await new Promise((resolve, reject) => {
bindingsStream.on('data', (bindings) => {
for (const term of bindings.values()) { // check for 1st value
@@ -329,9 +311,6 @@ function handleComunicaContextCreation(query) {
if (!query.comunicaContext.sources) {
query.comunicaContext.sources = [];
}
- if (query.comunicaContext.useProxy) {
- query.comunicaContext.httpProxyHandler = proxyHandler;
- }
}
}
@@ -343,7 +322,7 @@ async function getVariableOptions(query) {
handleComunicaContextCreation(query);
if (query.sourcesIndex) {
- const additionalSources = await getSourcesFromSourcesIndex(query.sourcesIndex, query.comunicaContext.useProxy);
+ const additionalSources = await getSourcesFromSourcesIndex(query.sourcesIndex, query.httpProxies);
query.comunicaContext.sources = [...new Set([...query.comunicaContext.sources, ...additionalSources])];
}
// END duplicated chunk of code
@@ -386,7 +365,7 @@ async function getVariableOptions(query) {
try {
for (const queryString of queryStringList) {
const bindingsStream = await comunicaEngineWrapper.queryBindings(queryString,
- { sources: query.comunicaContext.sources, httpProxyHandler: (query.comunicaContext.useProxy ? proxyHandler : undefined) });
+ { sources: query.comunicaContext.sources }, query.httpProxies);
await new Promise((resolve, reject) => {
bindingsStream.on('data', (bindings) => {
// see https://comunica.dev/docs/query/advanced/bindings/
diff --git a/main/src/lib/utils.js b/main/src/lib/utils.js
new file mode 100644
index 00000000..523d8594
--- /dev/null
+++ b/main/src/lib/utils.js
@@ -0,0 +1,19 @@
+/**
+ * Translate a url into the url to use with a given proxy, if applicable.
+ *
+ * Parameter httpProxies may specify a http proxy for urls starting with a given string.
+ *
+ * @param {string} url - the url
+ * @param {array} httpProxies - array of httpProxy definitions
+ * @returns {string}
+ */
+export function translateUrlToProxiedUrl(url, httpProxies) {
+ if (httpProxies) {
+ for (const entry of httpProxies) {
+ if (url.startsWith(entry.urlStart)) {
+ return `${entry.httpProxy}${url}`;
+ }
+ }
+ }
+ return url;
+}
\ No newline at end of file
diff --git a/test/cypress/e2e/custom-query-editor.cy.js b/test/cypress/e2e/custom-query-editor.cy.js
index a27f4cb1..898d5d02 100644
--- a/test/cypress/e2e/custom-query-editor.cy.js
+++ b/test/cypress/e2e/custom-query-editor.cy.js
@@ -63,11 +63,85 @@ ORDER BY ?componentName
cy.contains("https://www.example.com/data/component-c01").should('exist');
});
+ it("Create a new query, here an ASK query", () => {
+
+ cy.visit("/#/customQuery");
+
+ cy.get('input[name="name"]').type("Is there an artist etc...");
+ cy.get('textarea[name="description"]').type("Test an ASK query");
+
+ cy.get('textarea[name="queryString"]').clear();
+ cy.get('textarea[name="queryString"]').type(`PREFIX foaf:
+PREFIX dbo:
+PREFIX dbp:
+ASK WHERE {
+ ?person a dbo:Artist.
+ ?person foaf:name ?name.
+ ?person dbo:influencedBy dbp:Pablo_Picasso.
+}`);
+ cy.get('input[name="source"]').type("http://localhost:8080/example/artists");
+
+ cy.get('input[name="askQueryCheck"]').click()
+
+ cy.get('textarea[name="askQuery"]').clear()
+ cy.get('textarea[name="askQuery"]').type('"trueText":"Yes, there is at least one artist influenced by Picasso!","falseText":"No, there is not a single artist influenced by Picasso."}', { parseSpecialCharSequences: false })
+
+ cy.get('button[type="submit"]').click();
+
+ // Check faulty input error
+ cy.contains("Invalid Query. Check the JSON-Syntax");
+
+ cy.get('textarea[name="askQuery"]').clear()
+ cy.get('textarea[name="askQuery"]').type('{"trueText":"Yes, there is at least one artist influenced by Picasso!","falseText":"No, there is not a single artist influenced by Picasso."}', { parseSpecialCharSequences: false })
+
+ cy.get('button[type="submit"]').click();
+
+ // Check if the query works
+ cy.contains("Yes, there is at least one artist influenced by Picasso!")
+ });
+
+ it("Create a new query, here with http proxies", () => {
+
+ cy.visit("/#/customQuery");
+
+ cy.get('input[name="name"]').type("My idols custom...");
+ cy.get('textarea[name="description"]').type("Test a query wit http proxies");
+
+ cy.get('textarea[name="queryString"]').clear();
+ cy.get('textarea[name="queryString"]').type(`PREFIX schema:
+SELECT ?name ?birthDate_int WHERE {
+ ?list schema:name ?listTitle;
+ schema:itemListElement [
+ schema:name ?name;
+ schema:birthDate ?birthDate_int;
+ ].
+}`);
+ cy.get('input[name="source"]').type("http://localhost:8001/example/idols");
+
+ cy.get('input[name="httpProxiesCheck"]').click()
+
+ cy.get('textarea[name="httpProxies"]').clear()
+ cy.get('textarea[name="httpProxies"]').type('{"urlStart":"http://localhost:8001","httpProxy":"http://localhost:8000/"}, {"urlStart":"http://localhost:8002","httpProxy":"http://localhost:9000/"}]', { parseSpecialCharSequences: false })
+
+ cy.get('button[type="submit"]').click();
+
+ // Check faulty input error
+ cy.contains("Invalid Query. Check the JSON-Syntax");
+
+ cy.get('textarea[name="httpProxies"]').clear()
+ cy.get('textarea[name="httpProxies"]').type('[{"urlStart":"http://localhost:8001","httpProxy":"http://localhost:8000/"}, {"urlStart":"http://localhost:8002","httpProxy":"http://localhost:9000/"}]', { parseSpecialCharSequences: false })
+
+ cy.get('button[type="submit"]').click();
+
+ // Check if the query works
+ cy.contains("1-2 of 2");
+ });
+
it("Check if all possible parameters are filled in with parameterized URL", () => {
//templatedQueryCheck
// Navigate to the URL of a saved query with completely filled-in form
- cy.visit("/#/customQuery?name=Query+Name&description=Query+Description&queryString=Sparql+query+text&comunicaContextCheck=on&source=The+Comunica+Source&comunicaContext=%7B%22Advanced+Comunica+Context%22%3Atrue%7D&sourceIndexCheck=on&indexSourceUrl=Index+Source&indexSourceQuery=Index+Query+&askQueryCheck=on&askQuery=%7B%22trueText%22%3A%22+filled+in%22%2C%22falseText%22%3A%22not+filled+in%22%7D&directVariablesCheck=on&variables=%7B%22firstvariables%22%3A%5B%22only+one%22%5D%7D")
+ cy.visit("/#/customQuery?name=Query+Name&description=Query+Description&queryString=Sparql+query+text&comunicaContextCheck=on&source=The+Comunica+Source&comunicaContext=%7B%22Advanced+Comunica+Context%22%3Atrue%7D&sourceIndexCheck=on&indexSourceUrl=Index+Source&indexSourceQuery=Index+Query+&askQueryCheck=on&askQuery=%7B%22trueText%22%3A%22+filled+in%22%2C%22falseText%22%3A%22not+filled+in%22%7D&directVariablesCheck=on&variables=%7B%22firstvariables%22%3A%5B%22only+one%22%5D%7D&httpProxiesCheck=on&httpProxies=%5B%7B%22urlStart%22%3A%22http%3A%2F%2Flocalhost%3A8001%22%2C%22httpProxy%22%3A%22http%3A%2F%2Flocalhost%3A8000%2F%22%7D%5D")
// Verify that every field is correctly filled-in
cy.get('input[name="name"]').should('have.value', 'Query Name');
@@ -82,6 +156,8 @@ ORDER BY ?componentName
cy.get('textarea[name="askQuery"]').should('have.value', `{"trueText":" filled in","falseText":"not filled in"}`);
+ cy.get('textarea[name="httpProxies"]').should('have.value', `[{"urlStart":"http://localhost:8001","httpProxy":"http://localhost:8000/"}]`);
+
cy.get('textarea[name="variables"]').should('have.value', `{"firstvariables":["only one"]}`);
})
diff --git a/test/cypress/e2e/customize-existing-query.cy.js b/test/cypress/e2e/customize-existing-query.cy.js
index e806d9b6..d95543a0 100644
--- a/test/cypress/e2e/customize-existing-query.cy.js
+++ b/test/cypress/e2e/customize-existing-query.cy.js
@@ -159,6 +159,29 @@ SELECT DISTINCT ?source WHERE {
})
+ it("ASK query", () => {
+ cy.visit("/");
+ cy.contains("Example queries").click();
+ cy.contains("Is there an artist influenced by Picasso?").click();
+
+ cy.get('button').contains("Clone as custom query").click({ force: true }); // Button is out of FoV so we gotta force the click
+
+ cy.url().should('include', 'customQuery');
+
+ cy.get('textarea[name="askQuery"]').should('have.value', `{"trueText":"Yes, there is at least one artist influenced by Picasso!","falseText":"No, there is not a single artist influenced by Picasso."}`);
+ })
+
+ it("http proxies", () => {
+ cy.visit("/");
+ cy.contains("Example queries").click();
+ cy.contains("My idols").click();
+
+ cy.get('button').contains("Clone as custom query").click({ force: true }); // Button is out of FoV so we gotta force the click
+
+ cy.url().should('include', 'customQuery');
+
+ cy.get('textarea[name="httpProxies"]').should('have.value', `[{"urlStart":"http://localhost:8001","httpProxy":"http://localhost:8000/"}]`);
+ })
})
diff --git a/test/cypress/e2e/http-proxies.cy.js b/test/cypress/e2e/http-proxies.cy.js
new file mode 100644
index 00000000..8d28865a
--- /dev/null
+++ b/test/cypress/e2e/http-proxies.cy.js
@@ -0,0 +1,34 @@
+describe("Http proxies", () => {
+ it("With indirect sources and with source verification", () => {
+ cy.visit("/");
+
+ cy.contains("For testing only").click();
+ cy.contains("Http proxy test combined with indirect sources and source verification").click();
+
+ cy.get('[aria-label="Sources info"]').click();
+
+ cy.contains("1-3 of 3");
+
+ cy.contains("http://localhost:8080/verifiable-example/components-vc").parent('tr').within(() => {
+ cy.get('[aria-label="No authentication required"]').should("exist");
+ cy.get('[aria-label="Fetch was successful"]').should("exist");
+ cy.get('[aria-label="Verify source"]').click();
+ cy.get('[aria-label="Verification succeeded"]').should("exist");
+ });
+
+ cy.contains("http://localhost:8080/verifiable-example/components-vc-incorrect-proof").parent('tr').within(() => {
+ cy.get('[aria-label="No authentication required"]').should("exist");
+ cy.get('[aria-label="Fetch was successful"]').should("exist");
+ cy.get('[aria-label="Verify source"]').click();
+ cy.get('[aria-label="Verification failed"]').should("exist");
+ });
+
+ cy.contains("http://localhost:8080/example/components").parent('tr').within(() => {
+ cy.get('[aria-label="No authentication required"]').should("exist");
+ cy.get('[aria-label="Fetch was successful"]').should("exist");
+ cy.get('[aria-label="Verify source"]').click();
+ cy.get('[aria-label="No credential found to verify"]').should("exist");
+ });
+
+ });
+});
\ No newline at end of file
diff --git a/test/initial-pod-data/index-example-for-proxy-test-only$.ttl b/test/initial-pod-data/index-example-for-proxy-test-only$.ttl
new file mode 100644
index 00000000..691ec5bb
--- /dev/null
+++ b/test/initial-pod-data/index-example-for-proxy-test-only$.ttl
@@ -0,0 +1,7 @@
+@prefix rdfs: .
+
+<#index-example> rdfs:seeAlso
+ ,
+ ,
+
+.
diff --git a/test/initial-pod-data/index-example-for-proxy-test-only.acl b/test/initial-pod-data/index-example-for-proxy-test-only.acl
new file mode 100644
index 00000000..4696a32c
--- /dev/null
+++ b/test/initial-pod-data/index-example-for-proxy-test-only.acl
@@ -0,0 +1,15 @@
+@prefix acl: .
+@prefix foaf: .
+
+<#public>
+ a acl:Authorization;
+ acl:agentClass foaf:Agent;
+ acl:accessTo <./index-example-for-proxy-test-only>;
+ acl:mode acl:Read.
+
+<#owner>
+ a acl:Authorization;
+ acl:accessTo <./index-example-for-proxy-test-only>;
+ acl:agent ;
+ acl:mode acl:Read, acl:Write, acl:Control.
+