1
0
Fork 0

FEATURE: validate liquid templates on wizard save (#156)

* DEV: validate liquid templates on wizard save

* minor fix

* code improvements and spec

* version bump

* fixed failing specs

* FIX: handle displaying backend validation errors on frontend

* fixed linting

* improved error display

* validate raw description for steps

* refactor conditional

* Identify attribute with liquid template error and pass syntax error

Co-authored-by: angusmcleod <angus@mcleod.org.au>
Co-authored-by: Angus McLeod <angusmcleod@users.noreply.github.com>
Dieser Commit ist enthalten in:
Faizaan Gagan 2022-01-31 15:11:14 +05:30 committet von GitHub
Ursprung 5e5b5e67ee
Commit 51553bc71d
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
11 geänderte Dateien mit 155 neuen und 19 gelöschten Zeilen

Datei anzeigen

@ -58,6 +58,21 @@ export default Controller.extend({
} }
return wizardFieldList(steps); return wizardFieldList(steps);
}, },
getErrorMessage(result) {
if (result.backend_validation_error) {
return result.backend_validation_error;
}
let errorType = "failed";
let errorParams = {};
if (result.error) {
errorType = result.error.type;
errorParams = result.error.params;
}
return I18n.t(`admin.wizard.error.${errorType}`, errorParams);
},
actions: { actions: {
save() { save() {
@ -80,18 +95,7 @@ export default Controller.extend({
this.send("afterSave", result.wizard_id); this.send("afterSave", result.wizard_id);
}) })
.catch((result) => { .catch((result) => {
let errorType = "failed"; this.set("error", this.getErrorMessage(result));
let errorParams = {};
if (result.error) {
errorType = result.error.type;
errorParams = result.error.params;
}
this.set(
"error",
I18n.t(`admin.wizard.error.${errorType}`, errorParams)
);
later(() => this.set("error", null), 10000); later(() => this.set("error", null), 10000);
}) })

Datei anzeigen

@ -28,7 +28,7 @@ const CustomWizard = EmberObject.extend({
contentType: "application/json", contentType: "application/json",
data: JSON.stringify(data), data: JSON.stringify(data),
}).then((result) => { }).then((result) => {
if (result.error) { if (result.backend_validation_error) {
reject(result); reject(result);
} else { } else {
resolve(result); resolve(result);

Datei anzeigen

@ -51,6 +51,7 @@ en:
after_signup: "You can only have one 'after signup' wizard at a time. %{wizard_id} has 'after signup' enabled." after_signup: "You can only have one 'after signup' wizard at a time. %{wizard_id} has 'after signup' enabled."
after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard." after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard."
after_time: "After time setting is invalid." after_time: "After time setting is invalid."
liquid_syntax_error: "Liquid syntax error in %{attribute}: %{message}"
site_settings: site_settings:
custom_wizard_enabled: "Enable custom wizards." custom_wizard_enabled: "Enable custom wizards."

Datei anzeigen

@ -37,7 +37,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
wizard_id = template.save(create: params[:create]) wizard_id = template.save(create: params[:create])
if template.errors.any? if template.errors.any?
render json: failed_json.merge(errors: template.errors.full_messages) render json: failed_json.merge(backend_validation_error: template.errors.full_messages.join("\n\n"))
else else
render json: success_json.merge(wizard_id: wizard_id) render json: success_json.merge(wizard_id: wizard_id)
end end

Datei anzeigen

@ -20,10 +20,12 @@ class CustomWizard::TemplateValidator
data[:steps].each do |step| data[:steps].each do |step|
check_required(step, :step) check_required(step, :step)
validate_liquid_template(step, :step)
if step[:fields].present? if step[:fields].present?
step[:fields].each do |field| step[:fields].each do |field|
check_required(field, :field) check_required(field, :field)
validate_liquid_template(field, :field)
end end
end end
end end
@ -31,6 +33,7 @@ class CustomWizard::TemplateValidator
if data[:actions].present? if data[:actions].present?
data[:actions].each do |action| data[:actions].each do |action|
check_required(action, :action) check_required(action, :action)
validate_liquid_template(action, :action)
end end
end end
@ -95,4 +98,35 @@ class CustomWizard::TemplateValidator
errors.add :base, I18n.t("wizard.validation.after_time") errors.add :base, I18n.t("wizard.validation.after_time")
end end
end end
def validate_liquid_template(object, type)
%w[
description
raw_description
placeholder
preview_template
post_template
].each do |field|
if template = object[field]
result = is_liquid_template_valid?(template)
unless "valid" == result
error = I18n.t("wizard.validation.liquid_syntax_error",
attribute: "#{object[:id]}.#{field}",
message: result
)
errors.add :base, error
end
end
end
end
def is_liquid_template_valid?(template)
begin
Liquid::Template.parse(template)
'valid'
rescue Liquid::SyntaxError => error
error.message
end
end
end end

Datei anzeigen

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# name: discourse-custom-wizard # name: discourse-custom-wizard
# about: Create custom wizards # about: Create custom wizards
# version: 1.16.5 # version: 1.17.0
# authors: Angus McLeod # authors: Angus McLeod
# url: https://github.com/paviliondev/discourse-custom-wizard # url: https://github.com/paviliondev/discourse-custom-wizard
# contact emails: angus@thepavilion.io # contact emails: angus@thepavilion.io
@ -117,6 +117,8 @@ after_initialize do
load File.expand_path(path, __FILE__) load File.expand_path(path, __FILE__)
end end
Liquid::Template.error_mode = :strict
# preloaded category custom fields # preloaded category custom fields
%w[ %w[
create_topic_wizard create_topic_wizard

Datei anzeigen

@ -324,7 +324,7 @@ describe CustomWizard::Builder do
.build .build
.steps.first .steps.first
.fields.length .fields.length
).to eq(4) ).to eq(@template[:steps][0][:fields].length)
end end
context "with condition" do context "with condition" do

Datei anzeigen

@ -9,6 +9,33 @@ describe CustomWizard::TemplateValidator do
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
).read).with_indifferent_access ).read).with_indifferent_access
} }
let(:valid_liquid_template) {
<<-LIQUID.strip
{%- assign hello = "Topic Form 1" %}
LIQUID
}
let(:invalid_liquid_template) {
<<-LIQUID.strip
{%- assign hello = "Topic Form 1" %
LIQUID
}
let(:liquid_syntax_error) {
"Liquid syntax error: Tag '{%' was not properly terminated with regexp: /\\%\\}/"
}
def expect_validation_success
expect(
CustomWizard::TemplateValidator.new(template).perform
).to eq(true)
end
def expect_validation_failure(object_id, message)
validator = CustomWizard::TemplateValidator.new(template)
expect(validator.perform).to eq(false)
expect(validator.errors.first.message).to eq("Liquid syntax error in #{object_id}: #{message}")
end
it "validates valid templates" do it "validates valid templates" do
expect( expect(
@ -110,4 +137,64 @@ describe CustomWizard::TemplateValidator do
end end
end end
end end
context "liquid templates" do
it "validates if no liquid syntax in use" do
expect_validation_success
end
it "validates if liquid syntax in use is correct" do
template[:steps][0][:raw_description] = valid_liquid_template
expect_validation_success
end
it "doesn't validate if liquid syntax in use is incorrect" do
template[:steps][0][:raw_description] = invalid_liquid_template
expect_validation_failure("step_1.raw_description", liquid_syntax_error)
end
context "validation targets" do
context "fields" do
it "validates descriptions" do
template[:steps][0][:fields][0][:description] = invalid_liquid_template
expect_validation_failure("step_1_field_1.description", liquid_syntax_error)
end
it "validates placeholders" do
template[:steps][0][:fields][0][:placeholder] = invalid_liquid_template
expect_validation_failure("step_1_field_1.placeholder", liquid_syntax_error)
end
it "validates preview templates" do
template[:steps][0][:fields][4][:preview_template] = invalid_liquid_template
expect_validation_failure("step_1_field_5.preview_template", liquid_syntax_error)
end
end
context "steps" do
it "validates descriptions" do
template[:steps][0][:raw_description] = invalid_liquid_template
expect_validation_failure("step_1.raw_description", liquid_syntax_error)
end
end
context "actions" do
it "validates post builder" do
action = nil
action_index = nil
template[:actions].each_with_index do |a, i|
if a["post_builder"]
action = a
action_index = i
break
end
end
template[:actions][action_index][:post_template] = invalid_liquid_template
expect_validation_failure("#{action[:id]}.post_template", liquid_syntax_error)
end
end
end
end
end end

Datei anzeigen

@ -43,6 +43,13 @@
"label": "I'm only text", "label": "I'm only text",
"description": "", "description": "",
"type": "text_only" "type": "text_only"
},
{
"id": "step_1_field_5",
"label": "I'm a preview",
"description": "",
"type": "composer_preview",
"preview_template": "w{step_1_field_1}"
} }
], ],
"description": "Text inputs!" "description": "Text inputs!"

Datei anzeigen

@ -21,7 +21,7 @@ describe CustomWizard::FieldSerializer do
scope: Guardian.new(user) scope: Guardian.new(user)
).as_json ).as_json
expect(json_array.size).to eq(4) expect(json_array.size).to eq(@wizard.steps.first.fields.size)
expect(json_array[0][:label]).to eq("<p>Text</p>") expect(json_array[0][:label]).to eq("<p>Text</p>")
expect(json_array[0][:description]).to eq("Text field description.") expect(json_array[0][:description]).to eq("Text field description.")
expect(json_array[3][:index]).to eq(3) expect(json_array[3][:index]).to eq(3)

Datei anzeigen

@ -43,7 +43,8 @@ describe CustomWizard::StepSerializer do
each_serializer: described_class, each_serializer: described_class,
scope: Guardian.new(user) scope: Guardian.new(user)
).as_json ).as_json
expect(json_array[0][:fields].length).to eq(4)
expect(json_array[0][:fields].length).to eq(@wizard.steps[0].fields.length)
end end
context 'with required data' do context 'with required data' do