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 CamaleonCms::UnsafeMarkup's own constant 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 the ones the save-time gates themselves emit (
invalid_content,invalid_files_count) -- a key missing here is not merely un-offered, it is stripped from every editor save, so a message set any other way would be silently erased.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 invalid_files_count].freeze
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
Instance Method Details
#create ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 30 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
94 95 96 97 98 99 100 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 94 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
80 81 82 83 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 80 def destroy flash[:notice] = t('.deleted', default: 'Destroyed successfully').to_s if @form.destroy redirect_to action: :index end |
#edit ⇒ Object
25 26 27 28 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 25 def edit I18n.t('plugins.cama_contact_form.edit_view', default: 'Edit contact form') render 'edit' end |
#index ⇒ Object
18 19 20 21 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 18 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
104 105 106 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 104 def item_field render partial: 'item_field', locals: { field_type: params[:kind], cid: params[:cid] } end |
#manual ⇒ Object
102 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 102 def manual; end |
#responses ⇒ Object
85 86 87 88 89 90 91 92 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 85 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
23 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 23 def show; end |
#update ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'app/controllers/plugins/cama_contact_form/admin_forms_controller.rb', line 42 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 |