Addomg a fix for #4370 - allow queries to contain newlines, they will always be escaped.

This commit is contained in:
mike12345567 2022-02-07 17:56:01 +00:00
parent f625828b58
commit 9298071cc2
9 changed files with 83 additions and 30 deletions

View File

@ -52,7 +52,7 @@
{/if} {/if}
{#if query?.parameters?.length > 0} {#if query?.parameters?.length > 0}
<div> <div class="params">
<BindingBuilder <BindingBuilder
bind:customParams={parameters.queryParams} bind:customParams={parameters.queryParams}
queryBindings={query.parameters} queryBindings={query.parameters}
@ -70,3 +70,11 @@
{/if} {/if}
{/if} {/if}
</Layout> </Layout>
<style>
.params {
display: flex;
flex-direction: column;
gap: var(--spacing-l);
}
</style>

View File

@ -230,7 +230,6 @@ describe("/queries", () => {
}) })
describe("variables", () => { describe("variables", () => {
async function preview(datasource, fields) { async function preview(datasource, fields) {
return config.previewQuery(request, config, datasource, fields) return config.previewQuery(request, config, datasource, fields)
} }

View File

@ -161,10 +161,16 @@ class QueryRunner {
const responses = await Promise.all(dynamics) const responses = await Promise.all(dynamics)
for (let i = 0; i < foundVars.length; i++) { for (let i = 0; i < foundVars.length; i++) {
const variable = foundVars[i] const variable = foundVars[i]
parameters[variable.name] = processStringSync(variable.value, { parameters[variable.name] = processStringSync(
data: responses[i].rows, variable.value,
info: responses[i].extra, {
}) data: responses[i].rows,
info: responses[i].extra,
},
{
escapeNewlines: true,
}
)
// make sure its known that this uses dynamic variables in case it fails // make sure its known that this uses dynamic variables in case it fails
this.hasDynamicVariables = true this.hasDynamicVariables = true
} }
@ -188,6 +194,7 @@ class QueryRunner {
enrichedQuery[key] = processStringSync(fields[key], parameters, { enrichedQuery[key] = processStringSync(fields[key], parameters, {
noEscaping: true, noEscaping: true,
noHelpers: true, noHelpers: true,
escapeNewlines: true,
}) })
} else { } else {
enrichedQuery[key] = fields[key] enrichedQuery[key] = fields[key]

View File

@ -21,7 +21,7 @@ const HELPERS = [
// javascript helper // javascript helper
new Helper(HelperFunctionNames.JS, processJS, false), new Helper(HelperFunctionNames.JS, processJS, false),
// this help is applied to all statements // this help is applied to all statements
new Helper(HelperFunctionNames.ALL, value => { new Helper(HelperFunctionNames.ALL, (value, { __opts }) => {
if ( if (
value != null && value != null &&
typeof value === "object" && typeof value === "object" &&
@ -36,7 +36,11 @@ const HELPERS = [
if (value && value.string) { if (value && value.string) {
value = value.string value = value.string
} }
let text = new SafeString(value.replace(/&amp;/g, "&")) let text = value
if (__opts && __opts.escapeNewlines) {
text = value.replace(/\n/g, "\\n")
}
text = new SafeString(text.replace(/&amp;/g, "&"))
if (text == null || typeof text !== "string") { if (text == null || typeof text !== "string") {
return text return text
} }
@ -62,10 +66,14 @@ module.exports.HelperNames = () => {
) )
} }
module.exports.registerAll = handlebars => { module.exports.registerMinimum = handlebars => {
for (let helper of HELPERS) { for (let helper of HELPERS) {
helper.register(handlebars) helper.register(handlebars)
} }
}
module.exports.registerAll = handlebars => {
module.exports.registerMinimum(handlebars)
// register imported helpers // register imported helpers
externalHandlebars.registerAll(handlebars) externalHandlebars.registerAll(handlebars)
} }

View File

@ -1,5 +1,5 @@
const handlebars = require("handlebars") const handlebars = require("handlebars")
const { registerAll } = require("./helpers/index") const { registerAll, registerMinimum } = require("./helpers/index")
const processors = require("./processors") const processors = require("./processors")
const { atob, btoa } = require("./utilities") const { atob, btoa } = require("./utilities")
const manifest = require("../manifest.json") const manifest = require("../manifest.json")
@ -8,6 +8,7 @@ const { FIND_HBS_REGEX, FIND_DOUBLE_HBS_REGEX } = require("./utilities")
const hbsInstance = handlebars.create() const hbsInstance = handlebars.create()
registerAll(hbsInstance) registerAll(hbsInstance)
const hbsInstanceNoHelpers = handlebars.create() const hbsInstanceNoHelpers = handlebars.create()
registerMinimum(hbsInstanceNoHelpers)
const defaultOpts = { noHelpers: false, noEscaping: false } const defaultOpts = { noHelpers: false, noEscaping: false }
/** /**
@ -105,9 +106,7 @@ module.exports.processStringSync = (string, context, opts) => {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
try { try {
// finalising adds a helper, can't do this with no helpers string = processors.preprocess(string, opts)
const shouldFinalise = !opts.noHelpers
string = processors.preprocess(string, shouldFinalise)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand // this does not throw an error when template can't be fulfilled, have to try correct beforehand
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
const templateString = const templateString =
@ -119,8 +118,10 @@ module.exports.processStringSync = (string, context, opts) => {
return processors.postprocess( return processors.postprocess(
template({ template({
now: new Date(now).toISOString(), now: new Date(now).toISOString(),
__opts: opts,
...context, ...context,
}) }),
{ escapeNewlines: opts ? opts.escapeNewlines : false }
) )
} catch (err) { } catch (err) {
return input return input

View File

@ -2,7 +2,7 @@ const { FIND_HBS_REGEX } = require("../utilities")
const preprocessor = require("./preprocessor") const preprocessor = require("./preprocessor")
const postprocessor = require("./postprocessor") const postprocessor = require("./postprocessor")
function process(output, processors) { function process(output, processors, opts) {
for (let processor of processors) { for (let processor of processors) {
// if a literal statement has occurred stop // if a literal statement has occurred stop
if (typeof output !== "string") { if (typeof output !== "string") {
@ -15,24 +15,18 @@ function process(output, processors) {
continue continue
} }
for (let match of matches) { for (let match of matches) {
output = processor.process(output, match) output = processor.process(output, match, opts)
} }
} }
return output return output
} }
module.exports.preprocess = (string, finalise = true) => { module.exports.preprocess = (string, opts) => {
let processors = preprocessor.processors let processors = preprocessor.processors
// the pre-processor finalisation stops handlebars from ever throwing an error return process(string, processors, opts)
// might want to pre-process for other benefits but still want to see errors
if (!finalise) {
processors = processors.filter(
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
)
}
return process(string, processors)
} }
module.exports.postprocess = string => { module.exports.postprocess = string => {
return process(string, postprocessor.processors) let processors = postprocessor.processors
return process(string, processors)
} }

View File

@ -16,6 +16,8 @@ class Postprocessor {
} }
} }
module.exports.PostProcessorNames = PostProcessorNames
module.exports.processors = [ module.exports.processors = [
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => { new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => {
if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) { if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) {

View File

@ -16,8 +16,8 @@ class Preprocessor {
this.fn = fn this.fn = fn
} }
process(fullString, statement) { process(fullString, statement, opts) {
const output = this.fn(statement) const output = this.fn(statement, opts)
const idx = fullString.indexOf(statement) const idx = fullString.indexOf(statement)
return swapStrings(fullString, idx, statement.length, output) return swapStrings(fullString, idx, statement.length, output)
} }
@ -48,7 +48,8 @@ module.exports.processors = [
return statement return statement
}), }),
new Preprocessor(PreprocessorNames.FINALISE, statement => { new Preprocessor(PreprocessorNames.FINALISE, (statement, opts) => {
const noHelpers = opts && opts.noHelpers
let insideStatement = statement.slice(2, statement.length - 2) let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") { if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1) insideStatement = insideStatement.slice(1)
@ -63,7 +64,10 @@ module.exports.processors = [
return statement return statement
} }
} }
if (HelperNames().some(option => option.includes(possibleHelper))) { if (
!noHelpers &&
HelperNames().some(option => option.includes(possibleHelper))
) {
insideStatement = `(${insideStatement})` insideStatement = `(${insideStatement})`
} }
return `{{ all ${insideStatement} }}` return `{{ all ${insideStatement} }}`

View File

@ -59,3 +59,33 @@ describe("attempt some complex problems", () => {
expect(output).toBe("nulltest") expect(output).toBe("nulltest")
}) })
}) })
describe("check behaviour with newlines", () => {
const context = {
binding: `Hello
there`
}
it("should escape new line to \\n with double brace", async () => {
const hbs = JSON.stringify({
body: "{{ binding }}"
})
const output = await processString(hbs, context, { escapeNewlines: true })
expect(JSON.parse(output).body).toBe(context.binding)
})
it("should work the same with triple brace", async () => {
const hbs = JSON.stringify({
body: "{{{ binding }}}"
})
const output = await processString(hbs, context, { escapeNewlines: true })
expect(JSON.parse(output).body).toBe(context.binding)
})
it("should still work with helpers disabled", async () => {
const hbs = JSON.stringify({
body: "{{ binding }}"
})
const output = await processString(hbs, context, { escapeNewlines: true, noHelpers: true })
expect(JSON.parse(output).body).toBe(context.binding)
})
})