Skip to content

Commit

Permalink
Merge pull request from GHSA-f78j-4w3g-4q65
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoroth authored Mar 12, 2024
1 parent 1733422 commit d823d73
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 8 deletions.
11 changes: 3 additions & 8 deletions app/channels/stimulus_reflex/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ def receive(data)
selectors = (data["selectors"] || []).select(&:present?)
selectors = data["selectors"] = ["body"] if selectors.blank?
target = data["target"].to_s
reflex_name, method_name = target.split("#")
reflex_name = reflex_name.camelize
reflex_name = reflex_name.end_with?("Reflex") ? reflex_name : "#{reflex_name}Reflex"
factory = StimulusReflex::ReflexFactory.new(target)
reflex_class = factory.call
method_name = factory.method_name
arguments = (data["args"] || []).map { |arg| object_with_indifferent_access arg }
element = StimulusReflex::Element.new(data)
permanent_attribute_name = data["permanentAttributeName"]
Expand All @@ -31,7 +31,6 @@ def receive(data)

begin
begin
reflex_class = reflex_name.constantize.tap { |klass| raise ArgumentError.new("#{reflex_name} is not a StimulusReflex::Reflex") unless is_reflex?(klass) }
reflex = reflex_class.new(self,
url: url,
element: element,
Expand Down Expand Up @@ -109,10 +108,6 @@ def object_with_indifferent_access(object)
object
end

def is_reflex?(reflex_class)
reflex_class.ancestors.include? StimulusReflex::Reflex
end

def delegate_call_to_reflex(reflex, method_name, arguments = [])
method = reflex.method(method_name)
required_params = method.parameters.select { |(kind, _)| kind == :req }
Expand Down
1 change: 1 addition & 0 deletions lib/stimulus_reflex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require "stimulus_reflex/configuration"
require "stimulus_reflex/callbacks"
require "stimulus_reflex/reflex"
require "stimulus_reflex/reflex_factory"
require "stimulus_reflex/element"
require "stimulus_reflex/sanity_checker"
require "stimulus_reflex/broadcasters/broadcaster"
Expand Down
62 changes: 62 additions & 0 deletions lib/stimulus_reflex/reflex_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

class StimulusReflex::ReflexFactory
attr_reader :reflex_name, :method_name

def initialize(target)
reflex_name, method_name = target.split("#")
reflex_name = reflex_name.camelize
reflex_name = reflex_name.end_with?("Reflex") ? reflex_name : "#{reflex_name}Reflex"

@method_name = method_name
@reflex_name = reflex_name
end

def call
verify_method_name!

reflex_class
end

private

def verify_method_name!
return if default_reflex?

argument_error = ArgumentError.new("Reflex method '#{method_name}' is not defined on class '#{reflex_name}' or on any of its ancestors")

if reflex_method.nil?
raise argument_error
end

if !safe_ancestors.include?(reflex_method.owner)
raise argument_error
end
end

def reflex_class
@reflex_class ||= reflex_name.constantize.tap do |klass|
unless klass.ancestors.include?(StimulusReflex::Reflex)
raise ArgumentError.new("#{reflex_name} is not a StimulusReflex::Reflex")
end
end
end

def reflex_method
if reflex_class.public_instance_methods.include?(method_name.to_sym)
reflex_class.public_instance_method(method_name)
end
end

def default_reflex?
method_name == "default_reflex" && reflex_method.owner == ::StimulusReflex::Reflex
end

def safe_ancestors
# We want to include every class and module up to the `StimulusReflex::Reflex` class,
# but not the StimulusReflex::Reflex itself
reflex_class_index = reflex_class.ancestors.index(StimulusReflex::Reflex) - 1

reflex_class.ancestors.to(reflex_class_index)
end
end
79 changes: 79 additions & 0 deletions test/reflex_factory_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require_relative "test_helper"

class StimulusReflex::ReflexFactoryTest < ActionCable::Channel::TestCase
tests StimulusReflex::Channel

test "reflex class needs to be an ancestor of StimulusReflex::Reflex" do
exception = assert_raises(NameError) { StimulusReflex::ReflexFactory.new("Object#inspect").call }
assert_equal "uninitialized constant ObjectReflex Did you mean? ObjectSpace", exception.message.squish

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("NoReflex#no_reflex").call }
assert_equal "NoReflex is not a StimulusReflex::Reflex", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("No#no_reflex").call }
assert_equal "NoReflex is not a StimulusReflex::Reflex", exception.message
end

test "doesn't raise if owner of method is ancestor of reflex class and descendant of StimulusReflex::Reflex" do
assert_nothing_raised { StimulusReflex::ReflexFactory.new("ApplicationReflex#default_reflex").call }
assert_nothing_raised { StimulusReflex::ReflexFactory.new("ApplicationReflex#application_reflex").call }

assert_nothing_raised { StimulusReflex::ReflexFactory.new("PostReflex#default_reflex").call }
assert_nothing_raised { StimulusReflex::ReflexFactory.new("PostReflex#application_reflex").call }
assert_nothing_raised { StimulusReflex::ReflexFactory.new("PostReflex#post_reflex").call }

assert_nothing_raised { StimulusReflex::ReflexFactory.new("CounterReflex#default_reflex").call }
assert_nothing_raised { StimulusReflex::ReflexFactory.new("CounterReflex#application_reflex").call }
assert_nothing_raised { StimulusReflex::ReflexFactory.new("CounterReflex#increment").call }
end

test "raises if method is not owned by a descendant of StimulusReflex::Reflex" do
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("ApplicationReflex#itself").call }
assert_equal "Reflex method 'itself' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("ApplicationReflex#itself").call }
assert_equal "Reflex method 'itself' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("PostReflex#itself").call }
assert_equal "Reflex method 'itself' is not defined on class 'PostReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("PostReflex#binding").call }
assert_equal "Reflex method 'binding' is not defined on class 'PostReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("PostReflex#byebug").call }
assert_equal "Reflex method 'byebug' is not defined on class 'PostReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("PostReflex#debug").call }
assert_equal "Reflex method 'debug' is not defined on class 'PostReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("ApplicationReflex#post_reflex").call }
assert_equal "Reflex method 'post_reflex' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message
end

test "raises if method is a private method" do
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("ApplicationReflex#private_application_reflex").call }
assert_equal "Reflex method 'private_application_reflex' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("PostReflex#private_application_reflex").call }
assert_equal "Reflex method 'private_application_reflex' is not defined on class 'PostReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("PostReflex#private_post_reflex").call }
assert_equal "Reflex method 'private_post_reflex' is not defined on class 'PostReflex' or on any of its ancestors", exception.message

exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new("CounterReflex#private_post_reflex").call }
assert_equal "Reflex method 'private_post_reflex' is not defined on class 'CounterReflex' or on any of its ancestors", exception.message
end

test "safe_ancestors" do
reflex_factory = StimulusReflex::ReflexFactory.new("ApplicationReflex#default_reflex")
assert_equal [ApplicationReflex], reflex_factory.send(:safe_ancestors)

reflex_factory = StimulusReflex::ReflexFactory.new("PostReflex#default_reflex")
assert_equal [PostReflex, ApplicationReflex], reflex_factory.send(:safe_ancestors)

reflex_factory = StimulusReflex::ReflexFactory.new("CounterReflex#increment")
assert_equal [CounterReflex, CounterConcern, ApplicationReflex], reflex_factory.send(:safe_ancestors)
end
end
34 changes: 34 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,40 @@ def load!
end
end

class ApplicationReflex < StimulusReflex::Reflex
def application_reflex
end

private

def private_application_reflex
end
end

class PostReflex < ApplicationReflex
def post_reflex
end

private

def private_post_reflex
end
end

class NoReflex
def no_reflex
end
end

module CounterConcern
def increment
end
end

class CounterReflex < ApplicationReflex
include CounterConcern
end

class ActionDispatch::Request
def session
@session ||= SessionMock.new
Expand Down

0 comments on commit d823d73

Please sign in to comment.