Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

Note: This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2025-07-11

### Added

- New LargeTextField::Base module, which delays the initialization of the association(s), and made LargeTextField::Owner a simple wrapper around Base which includes Base and initializes the association immediately. This was required to handle regressions for use cases that included the module, but did not define any large text value fields

## [1.3.0] - 2025-07-09

### Added
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
PATH
remote: .
specs:
large_text_field (1.3.0)
large_text_field (1.4.0)
invoca-utils (~> 0.3)
rails (>= 6.0)

GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.7.3)

Check failure on line 11 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L11

actionpack Warning Message: https://discuss.rubyonrails.org/t/cve-2023-28362-possible-xss-via-user-supplied-values-to-redirect-to/83132 CVE: CVE-2023-28362 Severity: medium

Check failure on line 11 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L11

actionpack Warning Message: https://github.com/rails/rails/security/advisories/GHSA-fwhr-88qx-h9g7 CVE: CVE-2024-28103 Severity: medium

Check failure on line 11 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L11

actionpack Warning Message: https://github.com/rails/rails/security/advisories/GHSA-x76w-6vjr-8xgj CVE: CVE-2024-41128 Severity:

Check failure on line 11 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L11

actionpack Warning Message: https://github.com/rails/rails/security/advisories/GHSA-vfg9-r3fq-jvx4 CVE: CVE-2024-47887 Severity:

Check failure on line 11 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L11

actionpack Warning Message: https://github.com/rails/rails/security/advisories/GHSA-vfm5-rmrh-j26v CVE: CVE-2024-54133 Severity:
actionpack (= 6.1.7.3)

Check failure on line 12 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L12

activesupport Warning Message: https://github.com/rails/rails/releases/tag/v7.0.7.1 CVE: CVE-2023-38037 Severity: medium
activesupport (= 6.1.7.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.3)
actionpack (= 6.1.7.3)
activejob (= 6.1.7.3)
activerecord (= 6.1.7.3)

Check failure on line 19 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L19

activestorage Warning Message: https://discuss.rubyonrails.org/t/possible-sensitive-session-information-leak-in-active-storage/84945 CVE: CVE-2024-26144 Severity: medium
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
mail (>= 2.7.1)

Check failure on line 22 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L22

actionmailer Warning Message: https://github.com/rails/rails/security/advisories/GHSA-h47h-mwp9-c6q6 CVE: CVE-2024-47889 Severity:
actionmailer (6.1.7.3)
actionpack (= 6.1.7.3)
actionview (= 6.1.7.3)
Expand All @@ -29,16 +29,16 @@
rails-dom-testing (~> 2.0)
actionpack (6.1.7.3)
actionview (= 6.1.7.3)
activesupport (= 6.1.7.3)

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://discuss.rubyonrails.org/t/denial-of-service-vulnerability-in-rack-content-type-parsing/84941 CVE: CVE-2024-25126 Severity: medium

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://discuss.rubyonrails.org/t/possible-dos-vulnerability-with-range-header-in-rack/84944 CVE: CVE-2024-26141 Severity:

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://discuss.rubyonrails.org/t/possible-denial-of-service-vulnerability-in-rack-header-parsing/84942 CVE: CVE-2024-26146 Severity:

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://github.com/rack/rack/security/advisories/GHSA-7g2v-jj9q-g3rg CVE: CVE-2025-25184 Severity:

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://github.com/rack/rack/security/advisories/GHSA-8cgq-6mh2-7j6v CVE: CVE-2025-27111 Severity:

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://github.com/rack/rack/security/advisories/GHSA-7wqh-767x-r66v CVE: CVE-2025-27610 Severity: high

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj CVE: CVE-2025-32441 Severity: medium

Check failure on line 32 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L32

rack Warning Message: https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx CVE: CVE-2025-46727 Severity: high
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)

Check failure on line 36 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L36

actiontext Warning Message: https://github.com/rails/rails/security/advisories/GHSA-wwhv-wxv9-rpgw CVE: CVE-2024-47888 Severity:
actiontext (6.1.7.3)
actionpack (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)

Check failure on line 41 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L41

nokogiri Warning Message: https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-5w6v-399v-w3cc CVE: GHSA-5w6v-399v-w3cc Severity:

Check failure on line 41 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L41

nokogiri Warning Message: https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-mrxw-mxhj-p664 CVE: GHSA-mrxw-mxhj-p664 Severity: high

Check failure on line 41 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L41

nokogiri Warning Message: https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-r95h-9x8f-r3f7 CVE: GHSA-r95h-9x8f-r3f7 Severity:

Check failure on line 41 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L41

nokogiri Warning Message: https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-vvfq-8hwr-qm4m CVE: GHSA-vvfq-8hwr-qm4m Severity:

Check failure on line 41 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L41

nokogiri Warning Message: https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j CVE: GHSA-xc9x-jj77-9p9j Severity:
nokogiri (>= 1.8.5)
actionview (6.1.7.3)
activesupport (= 6.1.7.3)
Expand Down Expand Up @@ -96,7 +96,7 @@
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)

Check failure on line 99 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L99

net-imap Warning Message: https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69 CVE: CVE-2025-25186 Severity: medium

Check failure on line 99 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L99

net-imap Warning Message: https://github.com/ruby/net-imap/security/advisories/GHSA-j3g3-5qv5-52mj CVE: CVE-2025-43857 Severity:
net-imap
net-pop
net-smtp
Expand Down Expand Up @@ -164,7 +164,7 @@
thor (~> 1.0)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.0)

Check failure on line 167 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L167

rexml Warning Message: https://github.com/ruby/rexml/security/advisories/GHSA-vg3r-rm7w-2xgh CVE: CVE-2024-35176 Severity: medium

Check failure on line 167 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L167

rexml Warning Message: https://github.com/ruby/rexml/security/advisories/GHSA-4xqq-m2hx-25v8 CVE: CVE-2024-39908 Severity: medium

Check failure on line 167 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L167

rexml Warning Message: https://www.ruby-lang.org/en/news/2024/08/01/dos-rexml-cve-2024-41123 CVE: CVE-2024-41123 Severity: medium

Check failure on line 167 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L167

rexml Warning Message: https://www.ruby-lang.org/en/news/2024/08/01/dos-rexml-cve-2024-41946 CVE: CVE-2024-41946 Severity: medium

Check failure on line 167 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L167

rexml Warning Message: https://github.com/ruby/rexml/security/advisories/GHSA-vmwr-mc7x-5vc3 CVE: CVE-2024-43398 Severity: medium

Check failure on line 167 in Gemfile.lock

View check run for this annotation

Security Scanner as a Service / Bundle Audit

Gemfile.lock#L167

rexml Warning Message: https://github.com/ruby/rexml/security/advisories/GHSA-2rxp-v6pw-ch6m CVE: CVE-2024-49761 Severity: high
rexml (3.2.5)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
Expand Down
181 changes: 181 additions & 0 deletions lib/large_text_field/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# frozen_string_literal: true

require "invoca/utils"

module LargeTextField
module Base
extend ActiveSupport::Concern
include ActiveSupport::Callbacks

included do
validate :validate_large_text_fields
before_save :write_large_text_field_changes
define_callbacks :large_text_field_save

class_attribute :large_text_field_options
self.large_text_field_options = {}

class_attribute :initialized
self.initialized = false

class_attribute :large_text_field_class_name
self.large_text_field_class_name = "LargeTextField::NamedTextValue"
class_attribute :large_text_field_class
self.large_text_field_class = LargeTextField::NamedTextValue

class_attribute :large_text_field_deprecated_class_name
self.large_text_field_deprecated_class_name = nil
class_attribute :large_text_field_deprecated_class
self.large_text_field_deprecated_class = nil
end

def dup
result = super

result._clear_text_field_on_dup

large_text_field_options.each_key { |key| result.set_text_field(key, get_text_field(key)) }

result
end

def reload(options = nil)
super(options)

@text_field_hash = nil
self
end

def text_field_hash
unless @text_field_hash
@text_field_hash = large_text_fields.build_hash { |text_field| [text_field.field_name, text_field] }
if large_text_field_deprecated_class_name
deprecated_large_text_fields.each { |text_field| @text_field_hash[text_field.field_name] ||= text_field }
end
end
@text_field_hash
end

def text_field_hash_loaded?
defined?(@text_field_hash) && @text_field_hash.present?
end

def get_text_field(field_name)
text_field_hash[field_name.to_s]&.value || ''
end

def set_text_field(original_field_name, original_value)
!original_value.nil? or raise "LargeTextField value cannot be set value to nil."

field_name = original_field_name.to_s
value = original_value.is_a?(File) ? original_value.read : original_value.to_s
if (field = text_field_hash[field_name])
field.value = value
else
text_field_hash[field_name] = large_text_field_class.new(owner: self, field_name:, value:)
end
end

def text_field_changed(field_name)
text_field_hash_loaded? && @text_field_hash[field_name]&.changes&.any?
end

# rubocop:disable Metrics/CyclomaticComplexity
def validate_large_text_fields
if text_field_hash_loaded?
large_text_field_options.each do |key, options|
value = text_field_hash[key]&.value
conjugation = options[:singularize_errors] ? "is" : "are"
maximum = options[:maximum] || MAX_LENGTH

if value.present? && value.size > maximum
errors.add(
key,
"#{conjugation} too long (maximum is #{self.class.formatted_integer_value(maximum)} characters)"
)
end
end
end
end
# rubocop:enable Metrics/CyclomaticComplexity

def write_large_text_field_changes
run_callbacks(:large_text_field_save)

@text_field_hash = text_field_hash
.compact
.select { |_key, value| value.value.presence }
.transform_values { |value| value.is_a?(large_text_field_class) ? value : large_text_field_class.new(owner: self, field_name: value.field_name, value: value.value) }
self.large_text_fields = text_field_hash.values.compact
true
end

def _clear_text_field_on_dup
if instance_variable_defined?(:@text_field_hash)
remove_instance_variable(:@text_field_hash)
end
end

module ClassMethods
def large_text_field_class_name_override(value)
self.large_text_field_class_name = value
self.large_text_field_class = Object.const_get(value)
end

def large_text_field_deprecated_class_name_override(value)
self.large_text_field_deprecated_class_name = value
self.large_text_field_deprecated_class = Object.const_get(value)
end

def initialize_large_text_field
return if initialized # skip if already initialized

has_many(
:large_text_fields,
class_name: large_text_field_class_name,
as: :owner,
autosave: true,
dependent: :destroy,
inverse_of: :owner
)
if large_text_field_deprecated_class_name
has_many(
:deprecated_large_text_fields,
class_name: large_text_field_deprecated_class_name,
as: :owner,
autosave: true,
dependent: :destroy,
inverse_of: :owner
)
end
self.initialized = true
end

def large_text_field(field_name, maximum: nil, singularize_errors: false)
initialize_large_text_field # ensure the association is initialized

field_name = field_name.to_s

# validate custom maximum
if maximum
if !maximum.is_a? Integer
raise ArgumentError, "maximum must be a number"
elsif maximum > MAX_LENGTH
raise ArgumentError, "maximum can't be greater than #{formatted_integer_value(MAX_LENGTH)}"
elsif maximum.negative?
raise ArgumentError, "maximum can't be less than 0"
end
end

large_text_field_options[field_name] = { maximum:, singularize_errors: }
define_method(field_name) { get_text_field(field_name) }
define_method("#{field_name}=") { |value| set_text_field(field_name, value) }
define_method("#{field_name}_changed?") { text_field_changed(field_name) }
end

def formatted_integer_value(value)
value.to_i.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
end
end
end
end
171 changes: 3 additions & 168 deletions lib/large_text_field/owner.rb
Original file line number Diff line number Diff line change
@@ -1,181 +1,16 @@
# frozen_string_literal: true

require "invoca/utils"
require_relative "base"

module LargeTextField
module Owner
extend ActiveSupport::Concern
include ActiveSupport::Callbacks
include LargeTextField::Base

included do
validate :validate_large_text_fields
before_save :write_large_text_field_changes
define_callbacks :large_text_field_save

class_attribute :large_text_field_options
self.large_text_field_options = {}

class_attribute :initialized
self.initialized = false

class_attribute :large_text_field_class_name
self.large_text_field_class_name = "LargeTextField::NamedTextValue"
class_attribute :large_text_field_class
self.large_text_field_class = LargeTextField::NamedTextValue

class_attribute :large_text_field_deprecated_class_name
self.large_text_field_deprecated_class_name = nil
class_attribute :large_text_field_deprecated_class
self.large_text_field_deprecated_class = nil
end

def dup
result = super

result._clear_text_field_on_dup

large_text_field_options.each_key { |key| result.set_text_field(key, get_text_field(key)) }

result
end

def reload(options = nil)
super(options)

@text_field_hash = nil
self
end

def text_field_hash
unless @text_field_hash
@text_field_hash = large_text_fields.build_hash { |text_field| [text_field.field_name, text_field] }
if large_text_field_deprecated_class_name
deprecated_large_text_fields.each { |text_field| @text_field_hash[text_field.field_name] ||= text_field }
end
end
@text_field_hash
end

def text_field_hash_loaded?
defined?(@text_field_hash) && @text_field_hash.present?
end

def get_text_field(field_name)
text_field_hash[field_name.to_s]&.value || ''
end

def set_text_field(original_field_name, original_value)
!original_value.nil? or raise "LargeTextField value cannot be set value to nil."

field_name = original_field_name.to_s
value = original_value.is_a?(File) ? original_value.read : original_value.to_s
if (field = text_field_hash[field_name])
field.value = value
else
text_field_hash[field_name] = large_text_field_class.new(owner: self, field_name:, value:)
end
end

def text_field_changed(field_name)
text_field_hash_loaded? && @text_field_hash[field_name]&.changes&.any?
end

# rubocop:disable Metrics/CyclomaticComplexity
def validate_large_text_fields
if text_field_hash_loaded?
large_text_field_options.each do |key, options|
value = text_field_hash[key]&.value
conjugation = options[:singularize_errors] ? "is" : "are"
maximum = options[:maximum] || MAX_LENGTH

if value.present? && value.size > maximum
errors.add(
key,
"#{conjugation} too long (maximum is #{self.class.formatted_integer_value(maximum)} characters)"
)
end
end
end
end
# rubocop:enable Metrics/CyclomaticComplexity

def write_large_text_field_changes
run_callbacks(:large_text_field_save)

@text_field_hash = text_field_hash
.compact
.select { |_key, value| value.value.presence }
.transform_values { |value| value.is_a?(large_text_field_class) ? value : large_text_field_class.new(owner: self, field_name: value.field_name, value: value.value) }
self.large_text_fields = text_field_hash.values.compact
true
end

def _clear_text_field_on_dup
if instance_variable_defined?(:@text_field_hash)
remove_instance_variable(:@text_field_hash)
end
end

module ClassMethods
def large_text_field_class_name_override(value)
self.large_text_field_class_name = value
self.large_text_field_class = Object.const_get(value)
end

def large_text_field_deprecated_class_name_override(value)
self.large_text_field_deprecated_class_name = value
self.large_text_field_deprecated_class = Object.const_get(value)
end

def initialize_large_text_field
return if initialized # skip if already initialized

has_many(
:large_text_fields,
class_name: large_text_field_class_name,
as: :owner,
autosave: true,
dependent: :destroy,
inverse_of: :owner
)
if large_text_field_deprecated_class_name
has_many(
:deprecated_large_text_fields,
class_name: large_text_field_deprecated_class_name,
as: :owner,
autosave: true,
dependent: :destroy,
inverse_of: :owner
)
end
self.initialized = true
end

def large_text_field(field_name, maximum: nil, singularize_errors: false)
initialize_large_text_field # ensure the association is initialized

field_name = field_name.to_s

# validate custom maximum
if maximum
if !maximum.is_a? Integer
raise ArgumentError, "maximum must be a number"
elsif maximum > MAX_LENGTH
raise ArgumentError, "maximum can't be greater than #{formatted_integer_value(MAX_LENGTH)}"
elsif maximum.negative?
raise ArgumentError, "maximum can't be less than 0"
end
end

large_text_field_options[field_name] = { maximum:, singularize_errors: }
define_method(field_name) { get_text_field(field_name) }
define_method("#{field_name}=") { |value| set_text_field(field_name, value) }
define_method("#{field_name}_changed?") { text_field_changed(field_name) }
end

def formatted_integer_value(value)
value.to_i.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
end
initialize_large_text_field
end
end
end
2 changes: 1 addition & 1 deletion lib/large_text_field/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module LargeTextField
VERSION = "1.3.0"
VERSION = "1.4.0"
end
Loading