cama_contact_form 0.1.0 → 0.1.12
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 +204 -0
- data/Rakefile +1 -1
- data/app/assets/javascripts/plugins/cama_contact_form/admin_editor.js +4 -4
- data/app/controllers/concerns/plugins/cama_contact_form/contact_form_controller_concern.rb +124 -9
- data/app/controllers/plugins/cama_contact_form/admin_forms_controller.rb +648 -7
- data/app/controllers/plugins/cama_contact_form/front_controller.rb +17 -1
- data/app/helpers/plugins/cama_contact_form/main_helper.rb +94 -17
- data/app/models/plugins/cama_contact_form/cama_contact_form.rb +25 -3
- data/app/views/plugins/cama_contact_form/admin_forms/_item_field.html.erb +8 -1
- data/app/views/plugins/cama_contact_form/forms_shorcode.html.erb +7 -3
- data/config/camaleon_plugin.json +2 -2
- data/config/initializers/custom_models.rb +1 -1
- data/config/locales/es.yml +8 -1
- data/config/locales/zh-CN.yml +7 -0
- data/lib/cama_contact_form/version.rb +1 -1
- metadata +7 -83
- data/test/cama_contact_form_test.rb +0 -7
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/javascripts/application.js +0 -13
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/controllers/application_controller.rb +0 -5
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -29
- data/test/dummy/config/application.rb +0 -26
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/database.yml +0 -25
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -41
- data/test/dummy/config/environments/production.rb +0 -79
- data/test/dummy/config/environments/test.rb +0 -42
- data/test/dummy/config/initializers/assets.rb +0 -11
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -4
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/config/secrets.yml +0 -22
- data/test/dummy/config.ru +0 -4
- data/test/dummy/public/404.html +0 -67
- data/test/dummy/public/422.html +0 -67
- data/test/dummy/public/500.html +0 -66
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/navigation_test.rb +0 -8
- data/test/test_helper.rb +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3853b9d6da4e632d947929a801497c0ea6daa25eff3b08834adf1c1aa5d5b86
|
|
4
|
+
data.tar.gz: 2293350d6071301a7795322485e25402974f82ef2dfadd485833a3c7bf89c851
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6aff50b5f8576691862b332848774945591b00600bae4f0c36bd69852ce15dc90c92f2869b1a21a4d6b2e6bab2a6d623b2c1cd5ef93f70ce7265314d8c96112
|
|
7
|
+
data.tar.gz: e4f5b5977c96e550e3960d07b2fd4c5ae84ed2dae74c4e21f71036815f447476557580ba59c85738bd74f479a4580931d14f67688bfe47067f12d0d83571dcc4
|
data/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Camaleon CMS Plugin
|
|
2
|
+
This is a plugin for Camaleon CMS to manage Contact Forms
|
|
3
|
+
|
|
4
|
+
## Releasing
|
|
5
|
+
|
|
6
|
+
Releases are cut by the **Release** GitHub Actions workflow
|
|
7
|
+
([`.github/workflows/release_builder.yml`](.github/workflows/release_builder.yml)). It only runs
|
|
8
|
+
when you start it by hand — merging to `master` releases nothing. A single run does everything a
|
|
9
|
+
release needs, in this order:
|
|
10
|
+
|
|
11
|
+
1. verifies the release is safe to cut (see [What the workflow checks](#what-the-workflow-checks)),
|
|
12
|
+
2. builds the gem and inspects the built artifact,
|
|
13
|
+
3. pushes it to RubyGems,
|
|
14
|
+
4. creates the annotated git tag,
|
|
15
|
+
5. publishes the GitHub release, with notes taken from `CHANGELOG.md` and the `.gem` plus its
|
|
16
|
+
checksums attached.
|
|
17
|
+
|
|
18
|
+
No tests run here — the suite that covers this plugin lives in the
|
|
19
|
+
[camaleon_cms](https://github.com/owen2345/camaleon-cms) repository.
|
|
20
|
+
|
|
21
|
+
`lib/cama_contact_form/version.rb` is the single source of truth for the version. The tag, the
|
|
22
|
+
published gem and the GitHub release all derive from it, so they cannot disagree.
|
|
23
|
+
|
|
24
|
+
> **`0.1.12` below is only an example.** Every command, filename and heading in this section uses it
|
|
25
|
+
> as a stand-in for the version you are actually releasing — substitute yours as you go. This
|
|
26
|
+
> document is not updated each release, so do not expect the number here to be the next one.
|
|
27
|
+
|
|
28
|
+
### Before you start
|
|
29
|
+
|
|
30
|
+
- You need **write access** to this repository.
|
|
31
|
+
- The `RUBY_GEMS_PUBLISH_GEM_KEY` repository secret must exist — it is the RubyGems API key the
|
|
32
|
+
workflow publishes with. It is already configured; you never need to see or handle its value.
|
|
33
|
+
- You do **not** need RubyGems credentials on your own machine for the automated release. You only
|
|
34
|
+
need them for the [manual fallback](#fallback-releasing-entirely-by-hand).
|
|
35
|
+
|
|
36
|
+
### Step 1 — Open a release PR
|
|
37
|
+
|
|
38
|
+
Create a branch, bump the version, and describe the release:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git checkout master && git pull
|
|
42
|
+
git checkout -b release/0.1.12
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Edit `lib/cama_contact_form/version.rb`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
module CamaContactForm
|
|
49
|
+
VERSION = "0.1.12"
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then add a matching section at the top of `CHANGELOG.md`, directly under the `# Change Log`
|
|
54
|
+
heading. If an `## Unreleased` section is already sitting there, rename it to the version you are
|
|
55
|
+
shipping and fold your notes into it rather than adding a second section.
|
|
56
|
+
|
|
57
|
+
**The heading must be exactly `## 0.1.12`** — the workflow copies everything between that heading
|
|
58
|
+
and the next `## ` into the GitHub release body, so an inexact heading (`## v0.1.12`,
|
|
59
|
+
`## 0.1.12 (2026-08-01)`, or a leftover `## Unreleased`) silently costs you your release notes and
|
|
60
|
+
falls back to a bare commit list:
|
|
61
|
+
|
|
62
|
+
```markdown
|
|
63
|
+
## 0.1.12
|
|
64
|
+
|
|
65
|
+
What changed and why it matters to someone upgrading.
|
|
66
|
+
[#65](https://github.com/owen2345/cama_contact_form/pull/65)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Commit, push, open the PR, and merge it once it is approved:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git commit -am "Release 0.1.12"
|
|
73
|
+
git push -u origin release/0.1.12
|
|
74
|
+
gh pr create --title "Release 0.1.12" --body "Bumps the version and adds the changelog entry."
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Step 2 — Run the Release workflow
|
|
78
|
+
|
|
79
|
+
Merging does **not** publish anything. Once the PR is merged:
|
|
80
|
+
|
|
81
|
+
1. Open the repository on GitHub.
|
|
82
|
+
2. Click the **Actions** tab (in the top row, next to *Pull requests*).
|
|
83
|
+
3. In the left sidebar, under *All workflows*, click **Release**.
|
|
84
|
+
4. On the right-hand side of the blue banner, click the **Run workflow** dropdown button.
|
|
85
|
+
5. Leave **Use workflow from** as `Branch: master`. The workflow refuses to run from any other
|
|
86
|
+
branch, so a gem can never be published from unmerged code.
|
|
87
|
+
6. In **Version to release**, type the version exactly as it appears in `version.rb` — not the
|
|
88
|
+
`0.1.12` of this example. A leading `v` is accepted and stripped, and a mismatch fails the run
|
|
89
|
+
before anything is published.
|
|
90
|
+
7. Click the green **Run workflow** button.
|
|
91
|
+
|
|
92
|
+
The page takes a few seconds to show the new run; reload if it does not appear. Click into the run
|
|
93
|
+
to watch the three jobs — *Verify and build*, *Publish to RubyGems*, and *Tag and publish GitHub
|
|
94
|
+
release* — go green in turn.
|
|
95
|
+
|
|
96
|
+
> If the **Release** workflow is not listed in step 3, the workflow file has not reached `master`
|
|
97
|
+
> yet. GitHub only offers a manually-triggered workflow once its file exists on the default branch.
|
|
98
|
+
> Merge the PR first.
|
|
99
|
+
|
|
100
|
+
### Step 3 — Confirm
|
|
101
|
+
|
|
102
|
+
- **RubyGems**: <https://rubygems.org/gems/cama_contact_form/versions/0.1.12> — it can take a
|
|
103
|
+
minute or two to appear.
|
|
104
|
+
- **GitHub**: the repository's *Releases* page should show *Release 0.1.12* with your changelog
|
|
105
|
+
text and three attached files (`.gem`, `.sha256`, `.sha512`).
|
|
106
|
+
- Locally: `gem fetch cama_contact_form -v 0.1.12`
|
|
107
|
+
|
|
108
|
+
### What the workflow checks
|
|
109
|
+
|
|
110
|
+
Every check runs *before* anything is published, because pushing to RubyGems is the only step that
|
|
111
|
+
cannot be undone — a version can be yanked, but that version number can never be re-used. If a
|
|
112
|
+
check fails, nothing has been published and nothing has been tagged: fix the cause and start the
|
|
113
|
+
workflow again.
|
|
114
|
+
|
|
115
|
+
| Failure message | Cause | Fix |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `is not a valid gem version` | Typo in the input box | Re-run with the correct version |
|
|
118
|
+
| `Releases must be cut from master` | *Use workflow from* was not `master` | Re-run from `master` |
|
|
119
|
+
| `does not match lib/cama_contact_form/version.rb` | The typed version and `version.rb` disagree | Bump `version.rb`, or type the version it already has |
|
|
120
|
+
| `Tag <version> already exists` | That version was released before | Bump to a new version |
|
|
121
|
+
| `already published on RubyGems` | Same, but caught on the RubyGems side | Bump to a new version |
|
|
122
|
+
| `No release notes` | No `## <version>` section in `CHANGELOG.md` **and** no commits since the last tag | Add the changelog section |
|
|
123
|
+
| `gem build` fails under `--strict` | The gemspec has a warning (missing homepage, missing `required_ruby_version`, …) | Fix the gemspec |
|
|
124
|
+
| `Test files packaged into the gem` | `s.test_files` was re-added to the gemspec | Remove it — RubyGems merges `test_files` into `files` |
|
|
125
|
+
| `<file> is missing from the gem` | A filename in `s.files` no longer matches a real file | Correct the name in the gemspec |
|
|
126
|
+
|
|
127
|
+
The one failure that needs manual recovery is a job that fails *after* `gem push` succeeded — the
|
|
128
|
+
gem is public, but the tag and release were never created. Do not re-run the workflow, it would
|
|
129
|
+
stop at the "already published" check. Finish it by hand instead, using steps 5 and 6 of the
|
|
130
|
+
fallback below.
|
|
131
|
+
|
|
132
|
+
### Fallback: releasing entirely by hand
|
|
133
|
+
|
|
134
|
+
Use this if Actions is down, if the workflow is broken, or to finish a release that failed after
|
|
135
|
+
the gem was already pushed. It needs your own RubyGems credentials, and you must be an owner of the
|
|
136
|
+
gem on RubyGems.
|
|
137
|
+
|
|
138
|
+
As above, **`0.1.12` is a stand-in for the version you are releasing** — every command below needs
|
|
139
|
+
your own number substituted in, including the `.gem` filenames.
|
|
140
|
+
|
|
141
|
+
Sign in to RubyGems once per machine — this writes a token to `~/.gem/credentials`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
gem signin
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then, from a clean checkout of `master` with the correct version already in `version.rb`:
|
|
148
|
+
|
|
149
|
+
**1. Start from exactly what you intend to publish.**
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
git checkout master && git pull && git status
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`git status` must report a clean tree — `gem build` packages the files on disk, so any stray local
|
|
156
|
+
edit would end up in the published gem.
|
|
157
|
+
|
|
158
|
+
**2. Build the gem.**
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
gem build cama_contact_form.gemspec --strict
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**3. Inspect what you are about to publish.** This is the step the workflow automates; publishing
|
|
165
|
+
is irreversible, so it is worth the minute.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
gem unpack cama_contact_form-0.1.12.gem --target /tmp/gem-check
|
|
169
|
+
find /tmp/gem-check -type f | sort
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Expect about 26 files in total: `MIT-LICENSE`, `Rakefile`, `README.md`, and the rest under `app/`,
|
|
173
|
+
`config/`, `db/` and `lib/`. There must be **no `test/` directory** and no credentials of any kind.
|
|
174
|
+
|
|
175
|
+
**4. Push to RubyGems.** If your account has MFA enabled for publishing, this prompts for a
|
|
176
|
+
one-time code.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
gem push cama_contact_form-0.1.12.gem
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**5. Create and push the tag.** Annotated (`-a`), to match every existing tag in this repository:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
git tag -a 0.1.12 -m "Release 0.1.12"
|
|
186
|
+
git push origin 0.1.12
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**6. Publish the GitHub release.** Either with the CLI:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
gh release create 0.1.12 --title "Release 0.1.12" --notes-file notes.md cama_contact_form-0.1.12.gem
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
where `notes.md` holds the changelog section for this version — or through the web interface:
|
|
196
|
+
|
|
197
|
+
1. On the repository's front page, click **Releases** in the right-hand sidebar.
|
|
198
|
+
2. Click **Draft a new release**.
|
|
199
|
+
3. In **Choose a tag**, select the `0.1.12` tag you pushed in step 5.
|
|
200
|
+
4. Set **Target** to `master`.
|
|
201
|
+
5. Set the title to `Release 0.1.12`.
|
|
202
|
+
6. Paste the `CHANGELOG.md` section for this version into the description box.
|
|
203
|
+
7. Drag `cama_contact_form-0.1.12.gem` into the *Attach binaries* area at the bottom.
|
|
204
|
+
8. Click **Publish release**.
|
data/Rakefile
CHANGED
|
@@ -10,7 +10,7 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
|
10
10
|
rdoc.rdoc_dir = 'rdoc'
|
|
11
11
|
rdoc.title = 'CamaContactForm'
|
|
12
12
|
rdoc.options << '--line-numbers'
|
|
13
|
-
rdoc.rdoc_files.include('README.
|
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
|
14
14
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -16,7 +16,7 @@ $(function() {
|
|
|
16
16
|
res.find('.options_sortable').sortable({ handle: '.options-sortable' })
|
|
17
17
|
res.find('.add_option').click().click()
|
|
18
18
|
res.find('.translatable').Translatable(ADMIN_TRANSLATIONS)
|
|
19
|
-
|
|
19
|
+
hideLoading()
|
|
20
20
|
})
|
|
21
21
|
return false
|
|
22
22
|
})
|
|
@@ -27,7 +27,7 @@ $(function() {
|
|
|
27
27
|
|
|
28
28
|
const clone = list.children().first().clone().removeClass('hidden')
|
|
29
29
|
clone.find('input').prop('disabled', false).each(function() {
|
|
30
|
-
|
|
30
|
+
$(this).attr('name', $(this).attr('name').replace('[0]', '[' + list.attr('data-options-count') + ']'))
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
list.append(clone)
|
|
@@ -42,7 +42,7 @@ $(function() {
|
|
|
42
42
|
|
|
43
43
|
panel.on('click', '.panel-delete', function() {
|
|
44
44
|
$(this).closest('li.panel').fadeOut('slow', function() {
|
|
45
|
-
|
|
45
|
+
$(this).remove()
|
|
46
46
|
})
|
|
47
47
|
return false
|
|
48
48
|
})
|
|
@@ -53,5 +53,5 @@ $(function() {
|
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
myFields.find('.options_sortable').sortable({ handle: '.options-sortable' })
|
|
56
|
-
|
|
56
|
+
panel.find('.translatable').Translatable(ADMIN_TRANSLATIONS)
|
|
57
57
|
})
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
2
|
+
# The field types whose submitted value the renderer interpolates back into the page, and the
|
|
3
|
+
# position each one lands in. Everything else -- radio, checkboxes, dropdown, file -- is only ever
|
|
4
|
+
# compared against, never interpolated, so nothing it contains can escape anything.
|
|
5
|
+
ECHOED_ATTRIBUTE_FIELD_TYPES = %w[text website email].freeze
|
|
6
|
+
ECHOED_TEXTAREA_FIELD_TYPES = %w[paragraph textarea].freeze
|
|
7
|
+
|
|
8
|
+
# Elements that do something rather than say something, wherever they appear.
|
|
9
|
+
ACTIVE_ELEMENTS = %w[script style iframe object embed applet frame frameset form input button
|
|
10
|
+
link meta base svg math template].freeze
|
|
11
|
+
ACTIVE_ELEMENT = /<\s*\/?\s*(?:#{ACTIVE_ELEMENTS.join('|')})\b/i
|
|
12
|
+
EVENT_HANDLER_IN_TAG = /<[a-zA-Z][^>]*\son[a-zA-Z]+\s*=/im
|
|
13
|
+
URL_SCHEME_IN_TAG =
|
|
14
|
+
%r{<[a-zA-Z][^>]*\b(?:href|src|action|formaction|data|poster|srcdoc|background)\s*=\s*
|
|
15
|
+
["']?\s*(?:javascript|vbscript|data)\s*:}imx
|
|
16
|
+
|
|
2
17
|
def perform_save_form(form, fields, success, errors)
|
|
3
18
|
attachments = []
|
|
4
19
|
if validate_to_save_form(form, fields, errors)
|
|
@@ -26,14 +41,14 @@ module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
|
26
41
|
form_new = current_site.contact_forms.new(name: "response-#{Time.now}", description: form.description, settings: new_settings, site_id: form.site_id, parent_id: form.id)
|
|
27
42
|
if form_new.save
|
|
28
43
|
fields_data = convert_form_values(form, fields)
|
|
29
|
-
message_body = form.
|
|
30
|
-
content = render_to_string(partial: plugin_view('contact_form/email_content'), layout: false, formats: [
|
|
31
|
-
cama_send_email(form.
|
|
44
|
+
message_body = form.mail_settings[:body].to_s.translate.cama_replace_codes(fields)
|
|
45
|
+
content = render_to_string(partial: plugin_view('contact_form/email_content'), layout: false, formats: [:html], locals: {file_attachments: attachments, fields: fields_data, values: fields, message_body: message_body, form: form})
|
|
46
|
+
cama_send_email(form.mail_settings[:to], form.mail_settings[:subject].to_s.translate.cama_replace_codes(fields), {attachments: attachments, content: content, extra_data: {fields: fields_data}})
|
|
32
47
|
success << form.the_message('mail_sent_ok', t('.success_form_val', default: 'Your message has been sent successfully. Thank you very much!'))
|
|
33
48
|
args = {form: form, values: fields}; hooks_run("contact_form_after_submit", args)
|
|
34
|
-
if form.
|
|
35
|
-
content = form.
|
|
36
|
-
cama_send_email(answer_to, form.
|
|
49
|
+
if form.mail_settings[:to_answer].present? && (answer_to = fields[form.mail_settings[:to_answer].to_s.gsub(/(\[|\])/, '').to_sym]).present?
|
|
50
|
+
content = form.mail_settings[:body_answer].to_s.translate.cama_replace_codes(fields)
|
|
51
|
+
cama_send_email(answer_to, form.mail_settings[:subject_answer].to_s.translate.cama_replace_codes(fields), {content: content})
|
|
37
52
|
end
|
|
38
53
|
else
|
|
39
54
|
errors << form.the_message('mail_sent_ng', t('.error_form_val', default: 'An error occurred, please try again.'))
|
|
@@ -41,9 +56,107 @@ module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
|
41
56
|
end
|
|
42
57
|
end
|
|
43
58
|
|
|
59
|
+
# A visitor's submission is rejected, not escaped, for the same reason an untrusted author's is:
|
|
60
|
+
# what is stored and redisplayed then equals what was submitted, so rendering it verbatim adds
|
|
61
|
+
# nothing.
|
|
62
|
+
#
|
|
63
|
+
# Memoized because both the validation and the redisplay decision ask the same question about the
|
|
64
|
+
# same two objects in a single request, and the walk is over attacker-chosen structure.
|
|
65
|
+
def unsafe_submitted?(form, fields)
|
|
66
|
+
return @_cf_unsafe_submitted unless @_cf_unsafe_submitted.nil?
|
|
67
|
+
|
|
68
|
+
@_cf_unsafe_submitted = compute_unsafe_submitted?(form, fields)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Fails closed on a missing form: there is nothing to judge the submission against, and the caller
|
|
72
|
+
# must not go on to stash it.
|
|
73
|
+
def compute_unsafe_submitted?(form, fields)
|
|
74
|
+
return true if form.blank?
|
|
75
|
+
return true unless fields.is_a?(Hash) || fields.is_a?(ActionController::Parameters)
|
|
76
|
+
|
|
77
|
+
form.fields.any? { |f| unsafe_submitted_field?(f, fields[f[:cid].to_sym]) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Two sinks, judged separately because they are different contexts:
|
|
81
|
+
#
|
|
82
|
+
# The notification e-mail interpolates every submitted value into an author-written body and
|
|
83
|
+
# renders the result with `raw`, so a value carrying active markup is refused whatever its type.
|
|
84
|
+
# The test is deliberately narrow -- script, an event handler, a script URL -- and not a
|
|
85
|
+
# sanitizer, which would reject ordinary prose: a visitor writing `Fish & Chips <today>` would be
|
|
86
|
+
# told their message is not allowed, which is both wrong and infuriating.
|
|
87
|
+
#
|
|
88
|
+
# The redisplayed form interpolates the value only for the types listed above, and only into a
|
|
89
|
+
# double-quoted value attribute or into `<textarea>` content. A paragraph is RCDATA, where markup
|
|
90
|
+
# is literal text and only the closing tag ends it, so quotes and angle brackets are ordinary
|
|
91
|
+
# characters there.
|
|
92
|
+
def unsafe_submitted_field?(field, value)
|
|
93
|
+
return false if value.blank?
|
|
94
|
+
return true if submitted_leaves(value).any? { |leaf| active_markup?(leaf) }
|
|
95
|
+
|
|
96
|
+
type = field[:field_type].to_s
|
|
97
|
+
if ECHOED_TEXTAREA_FIELD_TYPES.include?(type)
|
|
98
|
+
echoed_value(value).downcase.include?('</textarea')
|
|
99
|
+
elsif ECHOED_ATTRIBUTE_FIELD_TYPES.include?(type)
|
|
100
|
+
echoed_value(value).include?('"')
|
|
101
|
+
else
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The exact string the renderer will emit. `values[cid]` is interpolated directly, so a non-String
|
|
107
|
+
# is judged by its `to_s` -- which is how an Array or a Hash supplies the double quote that closes
|
|
108
|
+
# the attribute without the payload needing one of its own. Judging the leaves instead of the whole
|
|
109
|
+
# missed that; judging `Array#to_s` for *every* type refused every checkbox and every file upload,
|
|
110
|
+
# because `Array#to_s` is `inspect` and always carries quotes.
|
|
111
|
+
def echoed_value(value)
|
|
112
|
+
value.is_a?(String) ? value : value.to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# An uploaded file is not redisplayed and has no string form worth inspecting; everything else is
|
|
116
|
+
# judged as the mail body will interpolate it.
|
|
117
|
+
def submitted_leaves(value)
|
|
118
|
+
case value
|
|
119
|
+
when ActionController::Parameters then submitted_leaves(value.to_unsafe_h)
|
|
120
|
+
when Hash then value.values.flat_map { |v| submitted_leaves(v) }
|
|
121
|
+
when Array then value.flat_map { |v| submitted_leaves(v) }
|
|
122
|
+
when ActionDispatch::Http::UploadedFile then []
|
|
123
|
+
else [value.to_s]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def active_markup?(string)
|
|
128
|
+
string = string.to_s
|
|
129
|
+
return false unless string.include?('<')
|
|
130
|
+
|
|
131
|
+
string.match?(ACTIVE_ELEMENT) || string.match?(EVENT_HANDLER_IN_TAG) || string.match?(URL_SCHEME_IN_TAG)
|
|
132
|
+
end
|
|
133
|
+
|
|
44
134
|
# form validations
|
|
45
135
|
def validate_to_save_form(form, fields, errors)
|
|
136
|
+
# Refuse outright and stop, before any other validation runs.
|
|
137
|
+
#
|
|
138
|
+
# Nothing is stored either way, so there is nothing to gain by continuing -- and continuing means
|
|
139
|
+
# running the rest of this method over input already known to be hostile, including a reCAPTCHA
|
|
140
|
+
# round-trip to an external service.
|
|
141
|
+
#
|
|
142
|
+
# The message names no field and quotes nothing back. The frontend flash partial renders with
|
|
143
|
+
# `raw`, so echoing the refused value there would make the refusal itself an injection sink --
|
|
144
|
+
# the same trap as the admin path.
|
|
145
|
+
if form.blank? || !(fields.is_a?(Hash) || fields.is_a?(ActionController::Parameters))
|
|
146
|
+
errors << t('.invalid_request_val', default: 'That form could not be submitted. Please try again.')
|
|
147
|
+
return false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if unsafe_submitted?(form, fields)
|
|
151
|
+
errors << form.the_message('invalid_content',
|
|
152
|
+
t('.invalid_content_val',
|
|
153
|
+
default: 'Your message contains characters that are not allowed. ' \
|
|
154
|
+
'Please remove any HTML or quotation marks and try again.'))
|
|
155
|
+
return false
|
|
156
|
+
end
|
|
157
|
+
|
|
46
158
|
validate = true
|
|
159
|
+
|
|
47
160
|
form.fields.each do |f|
|
|
48
161
|
cid = f[:cid].to_sym
|
|
49
162
|
label = f[:label].to_sym
|
|
@@ -53,8 +166,10 @@ module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
|
53
166
|
errors << "#{label.to_s.translate}: #{form.the_message('invalid_required', t('.error_validation_val', default: 'This value is required'))}"
|
|
54
167
|
validate = false
|
|
55
168
|
end
|
|
169
|
+
# `to_s` because the submitter chooses whether to send the key at all, and what shape to
|
|
170
|
+
# send it in: `nil.match` and `Hash#match` are both NoMethodError, on a public endpoint.
|
|
56
171
|
if f[:field_type].to_s == 'email'
|
|
57
|
-
unless fields[cid].match(/@/)
|
|
172
|
+
unless fields[cid].to_s.match(/@/)
|
|
58
173
|
errors << "#{label.to_s.translate}: #{form.the_message('invalid_email', t('.email_invalid_val', default: 'The e-mail address appears invalid'))}"
|
|
59
174
|
validate = false
|
|
60
175
|
end
|
|
@@ -86,10 +201,10 @@ module Plugins::CamaContactForm::ContactFormControllerConcern
|
|
|
86
201
|
label = values.keys.include?(field[:label]) ? "#{field[:label]} (#{cid})" : field[:label].to_s.translate
|
|
87
202
|
values[label] = []
|
|
88
203
|
if ft == 'file'
|
|
89
|
-
nr_files = fields[cid].size
|
|
204
|
+
nr_files = Array(fields[cid]).size
|
|
90
205
|
values[label] << "#{nr_files} #{"file".pluralize(nr_files)} (attached)" if fields[cid].present?
|
|
91
206
|
elsif ft == 'radio' || ft == 'checkboxes'
|
|
92
|
-
values[label] << fields[cid].map { |f| f.to_s.translate }.join(', ') if fields[cid].present?
|
|
207
|
+
values[label] << Array(fields[cid]).map { |f| f.to_s.translate }.join(', ') if fields[cid].present?
|
|
93
208
|
else
|
|
94
209
|
values[label] << fields[cid] if fields[cid].present?
|
|
95
210
|
end
|