Class: Plugins::CamaContactForm::AdminFormsController
- Inherits:
-
CamaleonCms::Apps::PluginsAdminController
- Object
- CamaleonCms::Apps::PluginsAdminController
- Plugins::CamaContactForm::AdminFormsController
- Includes:
- ContactFormControllerConcern, MainHelper
- Defined in:
- app/controllers/plugins/cama_contact_form/admin_forms_controller.rb
Overview
Admin CRUD for a site's contact forms; refuses (never rewrites) authored markup an untrusted role is not permitted to store.
Constant Summary collapse
- MARKUP_MAIL_KEYS =
Values the author writes that reach the page verbatim. Nothing here is rewritten on save: an author either holds :manage, :contact_form_unfiltered_html and their content is stored exactly as written, or the save is rejected and they are told which field to fix.
Two kinds of position, because they fail differently:
Markup positions reach the page as element content. Unsafe means the value is not markup this role may write, or is markup a browser would read differently from the way it was parsed here. Attribute positions are interpolated by the renderer inside a double-quoted HTML attribute. Unsafe means only that the value contains a double quote. Angle brackets, ampersands and apostrophes are harmless there, so they are allowed.Which position a value lands in is fixed rather than inferred, because the field
templateis authored too.placeholder_in_tag?refuses a template that puts[ci],[label ci]or[descr ci]inside a tag, so every substituted value is element content and every attribute in the emitted markup is one the renderer wrote with double quotes. That rule binds everyone, not just untrusted authors: without it a template written by a trusted author decides the context of a value written by an untrusted one, and the table below stops being true. %w[previous_html after_html body body_answer subject subject_answer].freeze
- MARKUP_FIELD_KEYS =
%w[label].freeze
- MARKUP_FIELD_OPTION_KEYS =
%w[template description].freeze
- ATTRIBUTE_FIELD_KEYS =
labelis also emitted intoname="..."by the "other" input of a radio/checkbox group. %w[label].freeze
- ATTRIBUTE_FIELD_OPTION_KEYS =
%w[field_class].freeze
- ATTRIBUTE_SETTING_KEYS =
Built with a symbol key by #update, unlike the string-keyed settings around it. It reaches the page as
data-sitekey="...", which the recaptcha gem interpolates without escaping. %i[recaptcha_site_key].freeze
- MAIL_SCALAR_KEYS =
Mail values the code indexes or interpolates. Held to a scalar for everyone:
to_answerreachesString#gsubin the front controller andsubjectreaches the mailer, both after a response row has already been persisted. %w[to subject body to_answer subject_answer body_answer previous_html after_html].freeze
- TEXTAREA_FIELD_TYPES =
The field types whose default_value is redisplayed as textarea content rather than in a value attribute, and the ones that carry a list of options. Kept in step with the renderer's
case. %w[paragraph textarea].freeze
- OPTION_FIELD_TYPES =
%w[radio checkboxes dropdown select].freeze
- EVENT_HANDLER_ATTR_NAME =
An attribute whose name is an event handler runs script whatever its value is. It breaks out of nothing, so neither the name-shape check nor the double-quote check below can see it:
onfocusis a perfectly well-formed attribute name carrying a perfectly quote-free value.A blunt
onprefix rather than a list of known handler names, which is what HTML sanitizers use: the handler set grows with the platform, and a list that has fallen behind fails open. /\Aon/i- HTML_ATTR_NAME =
/\A[a-zA-Z_:][-a-zA-Z0-9_:.]*\z/- URL_BEARING_ATTR_NAMES =
An attribute name is not enough on its own:
formactionon a submit button, orhrefon a link, executes whatever scheme its value names. These carry a URL, so their value gets a scheme check;styleis refused outright, because an untrusted author has no need of inline CSS andposition:fixedover the viewport is a UI-redressing primitive. %w[href src action formaction data poster srcdoc xlink:href background dynsrc lowsrc].freeze
- REFUSED_ATTR_NAMES =
%w[style].freeze
- URL_IGNORABLE_CHARS =
/[[:space:]\u0000-\u001F\u007F]/- DANGEROUS_URL_SCHEME =
/\A(?:javascript|vbscript|data):/i- MARKUP_TAGS =
The safe list CamaleonCms::UnsafeMarkup scrubs these positions against: Rails' own, widened with the layout elements these positions legitimately carry. Rails' list is tuned for prose and omits
label,section,fieldsetand the table elements -- which is why the plugin's own default template was refused by an earlier implementation.relandtargetare deliberately NOT added. Rails omits them on purpose:rel="opener"is the explicit opt-back-in to thewindow.openerhandle that browsers disable fortarget="_blank", and it hands an untrusted author the ability to repoint the visitor's original tab. (Rails::HTML5::SafeListSanitizer. + %w[label section article header footer main aside nav figure figcaption fieldset legend table caption colgroup col thead tbody tfoot tr td th time mark wbr picture source]).freeze
- MARKUP_ATTRS =
(Rails::HTML5::SafeListSanitizer.allowed_attributes + %w[id for colspan rowspan span role tabindex hidden]).freeze
- TRANSLATION_MARKER =
A well-formed translation marker (
<!--:-->,<!--:en-->).rendered_formsstrips these to produce the marker-free string the renderer emits, so the gate judges that form too. Reused from the core detector rather than re-spelled: the markers this strips and the ones CamaleonCms::UnsafeMarkup scans around must share one grammar for the gate to stay sound, so they share one constant. CamaleonCms::UnsafeMarkup::TRANSLATION_MARKER
- PLACEHOLDER =
/\[(?:ci|label ci|descr ci)\]/- TAG_SPAN =
One whole tag, quoted attribute values included, so a
>written inside an attribute does not look like the end of the tag. The closing quote is optional so an unterminated one runs to the end of the value rather than ending the match early. %r{<[a-zA-Z/!?][^"'>]*(?:(?:"[^"]*"?|'[^']*'?)[^"'>]*)*>?}- FIELD_TYPES =
Structural values, as opposed to authored content: the form builder generates them and nobody types markup into them. They are checked against an allowlist for everyone, including an author holding the unfiltered-HTML grant -- a field whose type is
text" onfocus="xis not a feature anyone wants, it is a corrupt record. Constraining them here is what lets the renderer emit them verbatim alongside everything else.selectis the legacy spelling ofdropdown; a form stored under an older version carries it. %w[text paragraph textarea website email radio checkboxes dropdown select captcha file submit button reset_button].freeze
- CID_FORMAT =
/\A[a-zA-Z0-9_-]+\z/- MAX_FIELDS =
Every gated value costs a parse, and both the number of values and their size are chosen by the caller. Without these an account holding nothing but :manage, :plugins can drive several thousand Loofah parses, or one multi-megabyte parse, from a single request.
200- MAX_OPTIONS_PER_FIELD =
100- MAX_GATED_VALUE_BYTES =
64 * 1024
- MESSAGE_KEYS =
The messages the form editor offers, plus
invalid_content, which the gate itself emits.Permitted rather than taken wholesale.
railscf_messageused to go straight from params into the gate, which runs a full Loofah parse per leaf -- so the number of parses was chosen by the caller. Permitting also stops unbounded junk being persisted intosettings.to_json. %w[mail_sent_ok mail_sent_ng validation_error invalid_required invalid_email captcha_not_match invalid_content].freeze
Class Method Summary collapse
-
.core_markup_detector_available? ⇒ Boolean
The markup gate delegates to CamaleonCms::UnsafeMarkup, which ships in camaleon_cms >= 2.9.3.
- .ensure_core_markup_detector! ⇒ Object
Instance Method Summary collapse
- #create ⇒ Object
- #del_response ⇒ Object
- #destroy ⇒ Object
- #edit ⇒ Object
- #index ⇒ Object
- #item_field ⇒ Object
- #manual ⇒ Object
- #responses ⇒ Object
- #show ⇒ Object
- #update ⇒ Object
Class Method Details
.core_markup_detector_available? ⇒ Boolean
The markup gate delegates to CamaleonCms::UnsafeMarkup, which ships in camaleon_cms >= 2.9.3. That
floor is not expressible as a gemspec dependency -- camaleon_cms depends on this gem, so a reverse
pin would be circular -- and camaleon_cms 2.9.2 pins cama_contact_form ~> 0.1.0, wide enough to
resolve this release against a core that lacks the detector. Fail fast and clearly at load, rather
than with a bare NameError deep inside the first untrusted save: without the detector nothing can
be gated, so the plugin must not run at all against an incompatible core.
15 16 17 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 15 def self.core_markup_detector_available? defined?(CamaleonCms::UnsafeMarkup) ? true : false end |
.ensure_core_markup_detector! ⇒ Object
19 20 21 22 23 24 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 19 def self.ensure_core_markup_detector! return if core_markup_detector_available? raise "cama_contact_form #{::CamaContactForm::VERSION} requires camaleon_cms >= 2.9.3 " \ '(CamaleonCms::UnsafeMarkup is unavailable).' end |
Instance Method Details
#create ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 43 def create @form = current_site.contact_forms.new(params.require(:plugins_cama_contact_form_cama_contact_form).permit(:name, :slug)) if @form.save flash[:notice] = t('.created', default: 'Created successfully').to_s redirect_to action: :edit, id: @form.id else flash[:error] = @form.errors..join(', ') redirect_to action: :index end end |
#del_response ⇒ Object
107 108 109 110 111 112 113 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 107 def del_response response = current_site.contact_forms.find_by(id: params[:response_id]) if response.present? && response.destroy flash[:notice] = t('.actions.msg_deleted', default: 'The response has been deleted').to_s end redirect_to action: :responses end |
#destroy ⇒ Object
93 94 95 96 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 93 def destroy flash[:notice] = t('.deleted', default: 'Destroyed successfully').to_s if @form.destroy redirect_to action: :index end |
#edit ⇒ Object
38 39 40 41 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 38 def edit I18n.t('plugins.cama_contact_form.edit_view', default: 'Edit contact form') render 'edit' end |
#index ⇒ Object
31 32 33 34 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 31 def index @forms = current_site.contact_forms.where(parent_id: nil).all @forms = @forms.paginate(page: params[:page], per_page: current_site.admin_per_page) end |
#item_field ⇒ Object
117 118 119 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 117 def item_field render partial: 'item_field', locals: { field_type: params[:kind], cid: params[:cid] } end |
#manual ⇒ Object
115 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 115 def manual; end |
#responses ⇒ Object
98 99 100 101 102 103 104 105 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 98 def responses I18n.t('plugins.cama_contact_form.list_responses', default: 'Contact form records') @form = current_site.contact_forms.where({ id: params[:admin_form_id] }).first values = JSON.parse(@form.value).to_sym @op_fields = values[:fields].select { |field| relevant_field? field } @forms = current_site.contact_forms.where({ parent_id: @form.id }) @forms = @forms.paginate(page: params[:page], per_page: current_site.admin_per_page) end |
#show ⇒ Object
36 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 36 def show; end |
#update ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 55 def update form_params = params.require(:plugins_cama_contact_form_cama_contact_form).permit(:name, :slug) settings = { 'railscf_mail' => params[:railscf_mail], 'railscf_message' => , 'railscf_form_button' => params[:railscf_form_button], recaptcha_site_key: params[:recaptcha_site_key], recaptcha_secret_key: params[:recaptcha_secret_key] } # The shape of `params` is chosen by the client, not by the form editor, and every check below # indexes into it. Verified first, for everyone, so that nothing downstream -- here or in the # renderer -- has to cope with a String where it expected a Hash. if (malformed = first_malformed_shape_key(settings)) return reject_save(t('.malformed_structure', field: malformed, default: 'A field has a malformed %{field}. Nothing was saved.')) end fields = submitted_fields # Checked before the record is touched, so a rejected save leaves the form exactly as it was # rather than persisting the name and slug and dropping everything else. if (malformed = first_malformed_structural_key(fields)) return reject_save(t('.malformed_structure', field: malformed, default: 'A field has a malformed %{field}. Nothing was saved.')) end if !trusted_for_unfiltered_html? && (rejected = first_unpermitted_html_key(settings, fields)) return reject_save((*rejected)) end if @form.update(form_params) @form.update({ settings: settings.to_json, value: { fields: fields }.to_json }) flash[:notice] = t('.updated_success', default: 'Updated successfully') redirect_to action: :edit, id: @form.id else edit end end |