Module: ConcernsOnRails::Models::Sanitizable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/concerns_on_rails/models/sanitizable.rb
Overview
Opt-in HTML sanitization for string attributes — defense-in-depth, NOT a substitute for Rails’ default output escaping.
ESCAPE-FIRST: Rails already HTML-escapes ‘<%= record.body %>` via SafeBuffer, so for ordinary columns you need nothing here. Reach for this concern only for the rare column you render as trusted HTML (`raw` / `html_safe`) or that must be kept plain text. For full user-authored rich text, prefer Action Text.
class Article < ApplicationRecord
include ConcernsOnRails::Models::Sanitizable
# DEFAULT (on: :read) — non-destructive. The stored column stays raw;
# a `sanitized_<field>` reader returns the cleaned value:
sanitizable :body, with: :safe_list # => article.sanitized_body
sanitizable :summary, with: :strip # => article.sanitized_summary
sanitizable :body, with: { tags: %w[b i a], attributes: %w[href] }
# EXPLICIT destructive opt-in — for plain-text-only columns only:
sanitizable :title, with: :strip, on: :write # overwrites in before_validation
end
WARNING: ‘on: :write` is lossy and irreversible — never use it on code, Markdown, math, or anything where `<` / `>` are legitimate. It is also bypassed by `update_column` / `update_all` / raw SQL, which skip callbacks. The non-destructive `on: :read` default is preferred.
Presets (the ‘with:` argument):
:strip — remove all tags, keep inner text (the default)
:safe_list — Rails' allow-list: keep formatting tags, drop <script> etc.
:no_links — strip only <a> tags, keep their text
:none — no-op (declare the field / reader without transforming)
Array — custom tag allow-list, e.g. with: %w[b i a]
Hash — { tags: [...], attributes: [...] } allow-list
Proc — used as-is (the caller owns the non-String guard)
Constant Summary collapse
- PRESETS =
Frozen, string-safe lambdas — non-String values pass through untouched, exactly like Normalizable::PRESETS. Each resolves its sanitizer through the shared Support helper (fully qualified, so there is no lexical-scope dependency and libgumbo is probed once, not per access) and always returns a plain String via #to_s, so a SafeBuffer is never persisted.
{ strip: ->(v) { v.is_a?(String) ? ConcernsOnRails::Support::HtmlSanitizers.full.sanitize(v).to_s : v }, safe_list: ->(v) { v.is_a?(String) ? ConcernsOnRails::Support::HtmlSanitizers.safe.sanitize(v).to_s : v }, no_links: ->(v) { v.is_a?(String) ? ConcernsOnRails::Support::HtmlSanitizers.link.sanitize(v).to_s : v }, none: ->(v) { v } }.freeze
Instance Method Summary collapse
-
#apply_sanitizations ⇒ Object
Only fields declared with on: :write are mutated; on: :read fields keep their raw column value and are exposed through their sanitized_ reader.
Instance Method Details
#apply_sanitizations ⇒ Object
Only fields declared with on: :write are mutated; on: :read fields keep their raw column value and are exposed through their sanitized_ reader.
136 137 138 139 140 141 142 143 144 145 |
# File 'lib/concerns_on_rails/models/sanitizable.rb', line 136 def apply_sanitizations self.class.sanitizable_rules.each do |field, rule| next unless rule[:on] == :write value = self[field] next if value.nil? self[field] = rule[:sanitizer].call(value) # plain String, never a SafeBuffer end end |