Skip to content

Commit 51553bc

Browse files
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>
1 parent 5e5b5e6 commit 51553bc

File tree

11 files changed

+155
-19
lines changed

11 files changed

+155
-19
lines changed

assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ export default Controller.extend({
5858
}
5959
return wizardFieldList(steps);
6060
},
61+
getErrorMessage(result) {
62+
if (result.backend_validation_error) {
63+
return result.backend_validation_error;
64+
}
65+
66+
let errorType = "failed";
67+
let errorParams = {};
68+
69+
if (result.error) {
70+
errorType = result.error.type;
71+
errorParams = result.error.params;
72+
}
73+
74+
return I18n.t(`admin.wizard.error.${errorType}`, errorParams);
75+
},
6176

6277
actions: {
6378
save() {
@@ -80,18 +95,7 @@ export default Controller.extend({
8095
this.send("afterSave", result.wizard_id);
8196
})
8297
.catch((result) => {
83-
let errorType = "failed";
84-
let errorParams = {};
85-
86-
if (result.error) {
87-
errorType = result.error.type;
88-
errorParams = result.error.params;
89-
}
90-
91-
this.set(
92-
"error",
93-
I18n.t(`admin.wizard.error.${errorType}`, errorParams)
94-
);
98+
this.set("error", this.getErrorMessage(result));
9599

96100
later(() => this.set("error", null), 10000);
97101
})

assets/javascripts/discourse/models/custom-wizard.js.es6

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const CustomWizard = EmberObject.extend({
2828
contentType: "application/json",
2929
data: JSON.stringify(data),
3030
}).then((result) => {
31-
if (result.error) {
31+
if (result.backend_validation_error) {
3232
reject(result);
3333
} else {
3434
resolve(result);

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ en:
5151
after_signup: "You can only have one 'after signup' wizard at a time. %{wizard_id} has 'after signup' enabled."
5252
after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard."
5353
after_time: "After time setting is invalid."
54+
liquid_syntax_error: "Liquid syntax error in %{attribute}: %{message}"
5455

5556
site_settings:
5657
custom_wizard_enabled: "Enable custom wizards."

controllers/custom_wizard/admin/wizard.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def save
3737
wizard_id = template.save(create: params[:create])
3838

3939
if template.errors.any?
40-
render json: failed_json.merge(errors: template.errors.full_messages)
40+
render json: failed_json.merge(backend_validation_error: template.errors.full_messages.join("\n\n"))
4141
else
4242
render json: success_json.merge(wizard_id: wizard_id)
4343
end

lib/custom_wizard/validators/template.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,20 @@ def perform
2020

2121
data[:steps].each do |step|
2222
check_required(step, :step)
23+
validate_liquid_template(step, :step)
2324

2425
if step[:fields].present?
2526
step[:fields].each do |field|
2627
check_required(field, :field)
28+
validate_liquid_template(field, :field)
2729
end
2830
end
2931
end
3032

3133
if data[:actions].present?
3234
data[:actions].each do |action|
3335
check_required(action, :action)
36+
validate_liquid_template(action, :action)
3437
end
3538
end
3639

@@ -95,4 +98,35 @@ def validate_after_time
9598
errors.add :base, I18n.t("wizard.validation.after_time")
9699
end
97100
end
101+
102+
def validate_liquid_template(object, type)
103+
%w[
104+
description
105+
raw_description
106+
placeholder
107+
preview_template
108+
post_template
109+
].each do |field|
110+
if template = object[field]
111+
result = is_liquid_template_valid?(template)
112+
113+
unless "valid" == result
114+
error = I18n.t("wizard.validation.liquid_syntax_error",
115+
attribute: "#{object[:id]}.#{field}",
116+
message: result
117+
)
118+
errors.add :base, error
119+
end
120+
end
121+
end
122+
end
123+
124+
def is_liquid_template_valid?(template)
125+
begin
126+
Liquid::Template.parse(template)
127+
'valid'
128+
rescue Liquid::SyntaxError => error
129+
error.message
130+
end
131+
end
98132
end

plugin.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22
# name: discourse-custom-wizard
33
# about: Create custom wizards
4-
# version: 1.16.5
4+
# version: 1.17.0
55
# authors: Angus McLeod
66
# url: https://github.com/paviliondev/discourse-custom-wizard
77
# contact emails: angus@thepavilion.io
@@ -117,6 +117,8 @@ def process_require_tree_discourse_directive(path = ".")
117117
load File.expand_path(path, __FILE__)
118118
end
119119

120+
Liquid::Template.error_mode = :strict
121+
120122
# preloaded category custom fields
121123
%w[
122124
create_topic_wizard

spec/components/custom_wizard/builder_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@
324324
.build
325325
.steps.first
326326
.fields.length
327-
).to eq(4)
327+
).to eq(@template[:steps][0][:fields].length)
328328
end
329329

330330
context "with condition" do

spec/components/custom_wizard/template_validator_spec.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,33 @@
99
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
1010
).read).with_indifferent_access
1111
}
12+
let(:valid_liquid_template) {
13+
<<-LIQUID.strip
14+
{%- assign hello = "Topic Form 1" %}
15+
LIQUID
16+
}
17+
18+
let(:invalid_liquid_template) {
19+
<<-LIQUID.strip
20+
{%- assign hello = "Topic Form 1" %
21+
LIQUID
22+
}
23+
24+
let(:liquid_syntax_error) {
25+
"Liquid syntax error: Tag '{%' was not properly terminated with regexp: /\\%\\}/"
26+
}
27+
28+
def expect_validation_success
29+
expect(
30+
CustomWizard::TemplateValidator.new(template).perform
31+
).to eq(true)
32+
end
33+
34+
def expect_validation_failure(object_id, message)
35+
validator = CustomWizard::TemplateValidator.new(template)
36+
expect(validator.perform).to eq(false)
37+
expect(validator.errors.first.message).to eq("Liquid syntax error in #{object_id}: #{message}")
38+
end
1239

1340
it "validates valid templates" do
1441
expect(
@@ -110,4 +137,64 @@
110137
end
111138
end
112139
end
140+
141+
context "liquid templates" do
142+
it "validates if no liquid syntax in use" do
143+
expect_validation_success
144+
end
145+
146+
it "validates if liquid syntax in use is correct" do
147+
template[:steps][0][:raw_description] = valid_liquid_template
148+
expect_validation_success
149+
end
150+
151+
it "doesn't validate if liquid syntax in use is incorrect" do
152+
template[:steps][0][:raw_description] = invalid_liquid_template
153+
expect_validation_failure("step_1.raw_description", liquid_syntax_error)
154+
end
155+
156+
context "validation targets" do
157+
context "fields" do
158+
it "validates descriptions" do
159+
template[:steps][0][:fields][0][:description] = invalid_liquid_template
160+
expect_validation_failure("step_1_field_1.description", liquid_syntax_error)
161+
end
162+
163+
it "validates placeholders" do
164+
template[:steps][0][:fields][0][:placeholder] = invalid_liquid_template
165+
expect_validation_failure("step_1_field_1.placeholder", liquid_syntax_error)
166+
end
167+
168+
it "validates preview templates" do
169+
template[:steps][0][:fields][4][:preview_template] = invalid_liquid_template
170+
expect_validation_failure("step_1_field_5.preview_template", liquid_syntax_error)
171+
end
172+
end
173+
174+
context "steps" do
175+
it "validates descriptions" do
176+
template[:steps][0][:raw_description] = invalid_liquid_template
177+
expect_validation_failure("step_1.raw_description", liquid_syntax_error)
178+
end
179+
end
180+
181+
context "actions" do
182+
it "validates post builder" do
183+
action = nil
184+
action_index = nil
185+
186+
template[:actions].each_with_index do |a, i|
187+
if a["post_builder"]
188+
action = a
189+
action_index = i
190+
break
191+
end
192+
end
193+
template[:actions][action_index][:post_template] = invalid_liquid_template
194+
195+
expect_validation_failure("#{action[:id]}.post_template", liquid_syntax_error)
196+
end
197+
end
198+
end
199+
end
113200
end

spec/fixtures/wizard.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
"label": "I'm only text",
4444
"description": "",
4545
"type": "text_only"
46+
},
47+
{
48+
"id": "step_1_field_5",
49+
"label": "I'm a preview",
50+
"description": "",
51+
"type": "composer_preview",
52+
"preview_template": "w{step_1_field_1}"
4653
}
4754
],
4855
"description": "Text inputs!"
@@ -576,4 +583,4 @@
576583
]
577584
}
578585
]
579-
}
586+
}

spec/serializers/custom_wizard/wizard_field_serializer_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
scope: Guardian.new(user)
2222
).as_json
2323

24-
expect(json_array.size).to eq(4)
24+
expect(json_array.size).to eq(@wizard.steps.first.fields.size)
2525
expect(json_array[0][:label]).to eq("<p>Text</p>")
2626
expect(json_array[0][:description]).to eq("Text field description.")
2727
expect(json_array[3][:index]).to eq(3)

0 commit comments

Comments
 (0)