cama_contact_form 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -3
- data/Rakefile +5 -20
- data/app/controllers/concerns/plugins/cama_contact_form/contact_form_controller_concern.rb +128 -68
- data/app/controllers/plugins/cama_contact_form/admin_forms_controller.rb +151 -220
- data/app/controllers/plugins/cama_contact_form/front_controller.rb +13 -5
- data/app/helpers/plugins/cama_contact_form/main_helper.rb +145 -108
- data/app/models/plugins/cama_contact_form/cama_contact_form.rb +36 -23
- data/config/initializers/custom_models.rb +4 -1
- data/config/routes.rb +18 -17
- data/lib/cama_contact_form/engine.rb +12 -0
- data/lib/cama_contact_form/version.rb +3 -1
- data/lib/cama_contact_form.rb +5 -5
- data/lib/tasks/cama_contact_form_tasks.rake +2 -0
- metadata +1 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0773dea2d0ad03b4c0179fc634372ab5fe9a6111da16fe0c187118475df052f6
|
|
4
|
+
data.tar.gz: 7868caf0afb73a4f508855521a1c4a15660a7786dbea9876303096402f198844
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9435100f0afd723a911aa6efee2fc5ed30610b7eea1a5f640eeb45d6e5ff92a7199d4360d599b0ebffcfc279e79fb17dff80135fb4db69e9d88233d356001ea8
|
|
7
|
+
data.tar.gz: 563ba96b2539b554971caee757e47ed840ac48918c7564993197095876d3435b74ac86974023caead09ebcb6742f979f86d1ab58e651d87f7b47e134a5036c66
|
data/README.md
CHANGED
|
@@ -15,8 +15,7 @@ release needs, in this order:
|
|
|
15
15
|
5. publishes the GitHub release, with notes taken from `CHANGELOG.md` and the `.gem` plus its
|
|
16
16
|
checksums attached.
|
|
17
17
|
|
|
18
|
-
No tests run here — the
|
|
19
|
-
[camaleon_cms](https://github.com/owen2345/camaleon-cms) repository.
|
|
18
|
+
No tests run here — the release workflow only builds and publishes the gem.
|
|
20
19
|
|
|
21
20
|
`lib/cama_contact_form/version.rb` is the single source of truth for the version. The tag, the
|
|
22
21
|
published gem and the GitHub release all derive from it, so they cannot disagree.
|
|
@@ -46,7 +45,7 @@ Edit `lib/cama_contact_form/version.rb`:
|
|
|
46
45
|
|
|
47
46
|
```ruby
|
|
48
47
|
module CamaContactForm
|
|
49
|
-
VERSION =
|
|
48
|
+
VERSION = '0.1.12'
|
|
50
49
|
end
|
|
51
50
|
```
|
|
52
51
|
|
data/Rakefile
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
begin
|
|
2
4
|
require 'bundler/setup'
|
|
3
5
|
rescue LoadError
|
|
@@ -14,24 +16,7 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
|
14
16
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
load 'rails/tasks/statistics.rake'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
# The plugin's behaviour is exercised host-side in the camaleon_cms repository (see README /
|
|
20
|
+
# CHANGELOG); this repo ships no test suite, so the dummy-app engine tasks and the default `test`
|
|
21
|
+
# task are intentionally absent. `Bundler::GemHelper.install_tasks` still provides build/install/release.
|
|
25
22
|
Bundler::GemHelper.install_tasks
|
|
26
|
-
|
|
27
|
-
require 'rake/testtask'
|
|
28
|
-
|
|
29
|
-
Rake::TestTask.new(:test) do |t|
|
|
30
|
-
t.libs << 'lib'
|
|
31
|
-
t.libs << 'test'
|
|
32
|
-
t.pattern = 'test/**/*_test.rb'
|
|
33
|
-
t.verbose = false
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
task default: :test
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
# Contact-form logic shared by the admin and front controllers: it gates authored markup at save
|
|
6
|
+
# time, and validates, stores and mails a visitor's submission.
|
|
1
7
|
module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
2
8
|
# The field types whose submitted value the renderer interpolates back into the page, and the
|
|
3
9
|
# position each one lands in. Everything else -- radio, checkboxes, dropdown, file -- is only ever
|
|
@@ -5,57 +11,103 @@ module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
|
5
11
|
ECHOED_ATTRIBUTE_FIELD_TYPES = %w[text website email].freeze
|
|
6
12
|
ECHOED_TEXTAREA_FIELD_TYPES = %w[paragraph textarea].freeze
|
|
7
13
|
|
|
14
|
+
# Field types whose submitted value is a list of chosen option labels, joined for the mail summary.
|
|
15
|
+
MULTI_VALUE_FIELD_TYPES = %w[radio checkboxes].freeze
|
|
16
|
+
|
|
8
17
|
# Elements that do something rather than say something, wherever they appear.
|
|
9
18
|
ACTIVE_ELEMENTS = %w[script style iframe object embed applet frame frameset form input button
|
|
10
19
|
link meta base svg math template].freeze
|
|
11
|
-
ACTIVE_ELEMENT =
|
|
20
|
+
ACTIVE_ELEMENT = %r{<\s*/?\s*(?:#{ACTIVE_ELEMENTS.join('|')})\b}i
|
|
12
21
|
EVENT_HANDLER_IN_TAG = /<[a-zA-Z][^>]*\son[a-zA-Z]+\s*=/im
|
|
13
22
|
URL_SCHEME_IN_TAG =
|
|
14
|
-
|
|
15
|
-
["']?\s*(?:javascript|vbscript|data)\s
|
|
23
|
+
/<[a-zA-Z][^>]*\b(?:href|src|action|formaction|data|poster|srcdoc|background)\s*=\s*
|
|
24
|
+
["']?\s*(?:javascript|vbscript|data)\s*:/imx
|
|
16
25
|
|
|
17
26
|
def perform_save_form(form, fields, success, errors)
|
|
18
27
|
attachments = []
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
fields[f[:cid].to_sym] = file_paths
|
|
28
|
+
return unless validate_to_save_form(form, fields, errors)
|
|
29
|
+
|
|
30
|
+
form.fields.each do |f|
|
|
31
|
+
next unless f[:field_type] == 'file'
|
|
32
|
+
|
|
33
|
+
file_paths = []
|
|
34
|
+
fields[f[:cid].to_sym].to_a.each do |file|
|
|
35
|
+
res = cama_tmp_upload(file, {
|
|
36
|
+
maximum: current_site.get_option('filesystem_max_size', 100).megabytes,
|
|
37
|
+
path: Rails.public_path.join('contact_form', current_site.id.to_s),
|
|
38
|
+
name: file.original_filename
|
|
39
|
+
})
|
|
40
|
+
if res[:error].present?
|
|
41
|
+
errors << res[:error].to_s.translate
|
|
42
|
+
else
|
|
43
|
+
attachments << res[:file_path]
|
|
44
|
+
file_paths << res[:file_path].sub(Rails.public_path.to_s, cama_root_url)
|
|
38
45
|
end
|
|
39
46
|
end
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
fields[f[:cid].to_sym] = file_paths
|
|
48
|
+
end
|
|
49
|
+
new_settings = { 'fields' => fields, 'created_at' => Time.now.utc.strftime('%Y-%m-%d %H:%M:%S').to_s }.to_json
|
|
50
|
+
form_new = current_site.contact_forms.new(name: "response-#{Time.now.utc}", description: form.description,
|
|
51
|
+
settings: new_settings, site_id: form.site_id, parent_id: form.id)
|
|
52
|
+
if form_new.save
|
|
53
|
+
fields_data = convert_form_values(form, fields)
|
|
54
|
+
message_body = form.mail_settings[:body].to_s.translate.cama_replace_codes(fields)
|
|
55
|
+
content = render_to_string(partial: plugin_view('contact_form/email_content'), layout: false, formats: [:html],
|
|
56
|
+
locals: { file_attachments: attachments, fields: fields_data, values: fields,
|
|
57
|
+
message_body: message_body, form: form })
|
|
58
|
+
cama_send_email(form.mail_settings[:to],
|
|
59
|
+
form.mail_settings[:subject].to_s.translate.cama_replace_codes(fields),
|
|
60
|
+
{ attachments: attachments, content: content, extra_data: { fields: fields_data } })
|
|
61
|
+
success << form.the_message('mail_sent_ok',
|
|
62
|
+
t('.success_form_val',
|
|
63
|
+
default: 'Your message has been sent successfully. Thank you very much!'))
|
|
64
|
+
args = { form: form, values: fields }
|
|
65
|
+
hooks_run('contact_form_after_submit', args)
|
|
66
|
+
if (answer_to = auto_reply_recipient(form, fields))
|
|
67
|
+
content = form.mail_settings[:body_answer].to_s.translate.cama_replace_codes(fields)
|
|
68
|
+
cama_send_email(answer_to, form.mail_settings[:subject_answer].to_s.translate.cama_replace_codes(fields),
|
|
69
|
+
{ content: content })
|
|
55
70
|
end
|
|
71
|
+
else
|
|
72
|
+
errors << form.the_message('mail_sent_ng',
|
|
73
|
+
t('.error_form_val', default: 'An error occurred, please try again.'))
|
|
56
74
|
end
|
|
57
75
|
end
|
|
58
76
|
|
|
77
|
+
# CF-1: the auto-reply ("confirmation e-mail") recipient is whatever the visitor typed into the field
|
|
78
|
+
# named by `to_answer`, so it is fully attacker-controlled -- unchecked, the feature sends mail from
|
|
79
|
+
# the site's own From address to anyone. The reply goes to the normalized address, or nowhere.
|
|
80
|
+
# Volume across submissions is a separate concern (CF-2, rate limiting).
|
|
81
|
+
#
|
|
82
|
+
# A refused present value is logged -- otherwise the response is indistinguishable from full
|
|
83
|
+
# success and a lost confirmation is undiagnosable. The line names the form, not the value: the
|
|
84
|
+
# value is hostile by hypothesis, and hostile bytes don't belong in the log stream. An absent or
|
|
85
|
+
# blank value stays silent, as it always has -- the visitor supplied nothing to refuse.
|
|
86
|
+
def auto_reply_recipient(form, fields)
|
|
87
|
+
return if form.mail_settings[:to_answer].blank?
|
|
88
|
+
|
|
89
|
+
value = fields[form.mail_settings[:to_answer].to_s.gsub(/(\[|\])/, '').to_sym]
|
|
90
|
+
address = normalized_email_address(value)
|
|
91
|
+
if address.nil? && value.present?
|
|
92
|
+
Rails.logger.warn("cama_contact_form: auto-reply for form #{form.id} skipped, recipient failed validation (CF-1)")
|
|
93
|
+
end
|
|
94
|
+
address
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The stripped value when it is a single, syntactically-valid address; nil otherwise. Surrounding
|
|
98
|
+
# whitespace is stripped -- a pasted or mobile-typed address often carries a stray space, and the
|
|
99
|
+
# mailer delivered those before this guard existed -- and what remains must match
|
|
100
|
+
# `URI::MailTo::EMAIL_REGEXP`, which is anchored and admits no CR/LF, inner whitespace or `,`/`;`,
|
|
101
|
+
# so one submission can neither header-inject a Bcc nor fan out to a recipient list. Refused with
|
|
102
|
+
# the attacks, deliberately: name-addr forms (`Jane <jane@example.com>` -- address lists share that
|
|
103
|
+
# grammar) and raw-unicode addresses (the regexp is ASCII-only; the punycode form passes). A
|
|
104
|
+
# non-String value (`fields[cid][]=…` arrives as an Array) is rejected through `to_s` rather than
|
|
105
|
+
# raising.
|
|
106
|
+
def normalized_email_address(value)
|
|
107
|
+
address = value.to_s.strip
|
|
108
|
+
address if address.match?(URI::MailTo::EMAIL_REGEXP)
|
|
109
|
+
end
|
|
110
|
+
|
|
59
111
|
# A visitor's submission is rejected, not escaped, for the same reason an untrusted author's is:
|
|
60
112
|
# what is stored and redisplayed then equals what was submitted, so rendering it verbatim adds
|
|
61
113
|
# nothing.
|
|
@@ -161,31 +213,38 @@ module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
|
161
213
|
cid = f[:cid].to_sym
|
|
162
214
|
label = f[:label].to_sym
|
|
163
215
|
case f[:field_type].to_s
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
216
|
+
when 'text', 'website', 'paragraph', 'textarea', 'email', 'radio', 'checkboxes', 'dropdown', 'file'
|
|
217
|
+
if f[:required].to_s.cama_true? && fields[cid].blank?
|
|
218
|
+
errors << "#{label.to_s.translate}: #{form.the_message('invalid_required',
|
|
219
|
+
t('.error_validation_val',
|
|
220
|
+
default: 'This value is required'))}"
|
|
221
|
+
validate = false
|
|
222
|
+
end
|
|
223
|
+
# Judged by the same rule as the auto-reply recipient (`normalized_email_address`, which is
|
|
224
|
+
# total on any submitted shape -- the submitter chooses whether to send the key at all, and
|
|
225
|
+
# in what shape), so the field validation and the send decision cannot disagree: a malformed
|
|
226
|
+
# address is an error the visitor can act on here, not a success message followed by a
|
|
227
|
+
# confirmation that silently never arrives.
|
|
228
|
+
if (f[:field_type].to_s == 'email') && normalized_email_address(fields[cid]).nil?
|
|
229
|
+
errors << "#{label.to_s.translate}: #{form.the_message('invalid_email',
|
|
230
|
+
t('.email_invalid_val',
|
|
231
|
+
default: 'The e-mail address appears invalid'))}"
|
|
232
|
+
validate = false
|
|
233
|
+
end
|
|
234
|
+
when 'captcha'
|
|
235
|
+
error_message = lambda {
|
|
236
|
+
errors << "#{label.to_s.translate}: #{form.the_message('captcha_not_match',
|
|
237
|
+
t('.captch_error_val',
|
|
238
|
+
default: 'The entered code is incorrect'))}"
|
|
239
|
+
validate = false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if form.recaptcha_enabled?
|
|
243
|
+
form.set_captcha_settings!
|
|
244
|
+
error_message.call unless verify_recaptcha
|
|
245
|
+
else
|
|
246
|
+
error_message.call unless cama_captcha_verified?
|
|
247
|
+
end
|
|
189
248
|
end
|
|
190
249
|
end
|
|
191
250
|
validate
|
|
@@ -196,23 +255,24 @@ module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
|
196
255
|
values = {}
|
|
197
256
|
form.fields.each do |field|
|
|
198
257
|
next unless relevant_field?(field)
|
|
258
|
+
|
|
199
259
|
ft = field[:field_type]
|
|
200
260
|
cid = field[:cid].to_sym
|
|
201
|
-
label = values.
|
|
261
|
+
label = values.key?(field[:label]) ? "#{field[:label]} (#{cid})" : field[:label].to_s.translate
|
|
202
262
|
values[label] = []
|
|
203
263
|
if ft == 'file'
|
|
204
264
|
nr_files = Array(fields[cid]).size
|
|
205
|
-
values[label] << "#{nr_files} #{
|
|
206
|
-
elsif ft
|
|
265
|
+
values[label] << "#{nr_files} #{'file'.pluralize(nr_files)} (attached)" if fields[cid].present?
|
|
266
|
+
elsif MULTI_VALUE_FIELD_TYPES.include?(ft)
|
|
207
267
|
values[label] << Array(fields[cid]).map { |f| f.to_s.translate }.join(', ') if fields[cid].present?
|
|
208
|
-
|
|
209
|
-
values[label] << fields[cid]
|
|
268
|
+
elsif fields[cid].present?
|
|
269
|
+
values[label] << fields[cid]
|
|
210
270
|
end
|
|
211
271
|
end
|
|
212
272
|
values
|
|
213
273
|
end
|
|
214
274
|
|
|
215
275
|
def relevant_field?(field)
|
|
216
|
-
|
|
276
|
+
%w[captcha submit button].exclude?(field[:field_type])
|
|
217
277
|
end
|
|
218
278
|
end
|