Skip to content

Commit

Permalink
Fixes #5060 - Webhook custom payload not support complex object attri…
Browse files Browse the repository at this point in the history
…bute values.
  • Loading branch information
dominikklein committed May 27, 2024
1 parent d85e573 commit 4729760
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 14 deletions.
41 changes: 33 additions & 8 deletions app/jobs/trigger_webhook_job/custom_payload/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ module TriggerWebhookJob::CustomPayload::Parser

private

STRING_LIKE_CLASSES = %w[
String
ActiveSupport::TimeWithZone
ActiveSupport::Duration
].freeze

# This module validates the scanned replacement variables.
def parse(variables, tracks)
mappings = {}
Expand All @@ -29,19 +35,38 @@ def parse(variables, tracks)
# payload is valid JSON.
def replace(record, mappings)
mappings.each do |variable, value|
record.gsub!("\#{#{variable}}", value
.to_s
.gsub(%r{"}, '\"')
.gsub(%r{\n}, '\n')
.gsub(%r{\r}, '\r')
.gsub(%r{\t}, '\t')
.gsub(%r{\f}, '\f')
.gsub(%r{\v}, '\v'))
escaped_variable = Regexp.escape(variable)
pattern = %r{("\#\{#{escaped_variable}\}"|\#\{#{escaped_variable}\})}

is_string_like = value.class.to_s.in?(STRING_LIKE_CLASSES)

record.gsub!(pattern) do |match|
if match.start_with?('"')
escaped_value = escape_replace_value(value, is_string_like:)
is_string_like ? "\"#{escaped_value}\"" : escaped_value
else
escape_replace_value(value, is_string_like: true)
end
end
end

record
end

def escape_replace_value(value, is_string_like: false)
if is_string_like
value.to_s
.gsub(%r{"}, '\"')
.gsub(%r{\n}, '\n')
.gsub(%r{\r}, '\r')
.gsub(%r{\t}, '\t')
.gsub(%r{\f}, '\f')
.gsub(%r{\v}, '\v')
else
value.to_json
end
end

# Scan the custom payload for replacement variables.
def scan(record)
placeholders = record.scan(%r{(#\{[a-z_.?!]+\})}).flatten.uniq
Expand Down
23 changes: 23 additions & 0 deletions app/jobs/trigger_webhook_job/custom_payload/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ module TriggerWebhookJob::CustomPayload::Validator
Integer
String
Float
FalseClass
TrueClass
].freeze

ALLOWED_RAILS_CLASSES = %w[
ActiveSupport::TimeWithZone
ActiveSupport::Duration
].freeze

ALLOWED_CONTAINER_CLASSES = %w[
Hash
Array
].freeze

ALLOWED_DEFAULT_CLASSES = ALLOWED_SIMPLE_CLASSES + ALLOWED_RAILS_CLASSES

# This method executes the replacement variables and executes on any error,
Expand Down Expand Up @@ -47,11 +54,27 @@ def validate_methods!(methods, reference, display)

# Final value must be one of the above described classes.
def validate_value!(value, display)
return validate_container_values(value) if value.class.to_s.in?(ALLOWED_CONTAINER_CLASSES)
return "\#{#{display} / no such method}" if !value.class.to_s.in?(ALLOWED_DEFAULT_CLASSES)

value
end

def validate_container_values(container)
case container.class.to_s
when 'Array'
container.each_with_index do |value, index|
container[index] = value.class.to_s.in?(ALLOWED_DEFAULT_CLASSES) ? value : 'no such item'
end
when 'Hash'
container.each do |key, value|
container[key] = value.class.to_s.in?(ALLOWED_DEFAULT_CLASSES) ? value : 'no such item'
end
end

container
end

# Any top level object must be provided by the tracks hash (ticket, article,
# notification by default, any further information is related to the webhook
# content).
Expand Down
95 changes: 89 additions & 6 deletions spec/jobs/trigger_webhook_job/custom_payload_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@

context 'when the placeholder contains valid object and method' do
let(:record) { { 'ticket.id' => '#{ticket.id}' }.to_json }
let(:json_data) { { 'ticket.id' => ticket.id.to_s } }
let(:json_data) { { 'ticket.id' => ticket.id } }

it 'returns the determined value' do
expect(generate).to eq(json_data)
Expand Down Expand Up @@ -159,10 +159,20 @@
end

context 'when the placeholder contains multiple attributes' do
let(:record) { { 'my_field' => '#{ticket.id} // #{ticket.group.name}' }.to_json }
let(:record) do
{
'my_field' => 'Test #{ticket.id} // #{ticket.group.name} Test',
'my_field2' => '#{ticket.id} // #{ticket.group.name} Test',
'my_field3' => '#{ticket.id}',
'my_field4' => '#{ticket.group.name}',
}.to_json
end
let(:json_data) do
{
'my_field' => "#{ticket.id} // #{ticket.group.name}",
'my_field' => "Test #{ticket.id} // #{ticket.group.name} Test",
'my_field2' => "#{ticket.id} // #{ticket.group.name} Test",
'my_field3' => ticket.id,
'my_field4' => ticket.group.name.to_s,
}
end

Expand All @@ -184,7 +194,8 @@
'created_at' => '#{article.created_at}',
'subject' => '#{article.subject}',
'body' => '#{article.body}',
'attachments' => '#{article.attachments}'
'attachments' => '#{article.attachments}',
'internal' => '#{article.internal}',
}
}
}.to_json
Expand All @@ -193,22 +204,24 @@
{
'current_user' => '#{current_user / no such object}',
'ticket' => {
'id' => ticket.id.to_s,
'id' => ticket.id,
'owner' => ticket.owner.fullname.to_s,
'group' => ticket.group.name.to_s,
'article' => {
'id' => article.id.to_s,
'id' => article.id,
'created_at' => article.created_at.to_s,
'subject' => article.subject.to_s,
'body' => article.body.to_s,
'attachments' => '#{article.attachments / no such method}',
'internal' => article.internal
}
}
}
end

it 'returns a valid JSON payload' do
expect(generate).to eq(json_data)

end
end

Expand All @@ -222,6 +235,76 @@
end
end

context 'when object attributes are used in the placeholder', db_strategy: :reset do
let(:ticket) { create(:ticket, object_manager_attribute_name => object_manager_attribute_value) }

before do
create_object_manager_attribute
ObjectManager::Attribute.migration_execute
end

shared_examples 'check different usage' do
context 'when used in string context' do
let(:record) do
{
"ticket.#{object_manager_attribute_name}" => "Test \#{ticket.#{object_manager_attribute_name}}"
}.to_json
end
let(:json_data) do
{
"ticket.#{object_manager_attribute_name}" => "Test #{object_manager_attribute_value}"
}
end

it 'returns the determined value' do
expect(generate).to eq(json_data)
end
end

context 'when used in direct context' do
let(:record) do
{
"ticket.#{object_manager_attribute_name}" => "\#{ticket.#{object_manager_attribute_name}}"
}.to_json
end
let(:json_data) do
{
"ticket.#{object_manager_attribute_name}" => object_manager_attribute_value
}
end

it 'returns the determined value' do
expect(generate).to eq(json_data)
end
end
end

context 'when multiselect is used inside the ticket' do
let(:object_manager_attribute_name) { 'multiselect' }
let(:object_manager_attribute_value) { %w[key_1 key_3] }
let(:create_object_manager_attribute) do
create(:object_manager_attribute_multiselect, name: object_manager_attribute_name)
end

include_examples 'check different usage'
end

context 'when external data source is used inside the ticket', db_adapter: :postgresql do
let(:object_manager_attribute_name) { 'autocompletion_ajax_external_data_source' }
let(:object_manager_attribute_value) do
{
'value' => 123,
'label' => 'Example',
}
end
let(:create_object_manager_attribute) do
create(:object_manager_attribute_autocompletion_ajax_external_data_source, name: object_manager_attribute_name)
end

include_examples 'check different usage'
end
end

describe "when the placeholder contains object 'notification'" do
let(:record) do
{
Expand Down

0 comments on commit 4729760

Please sign in to comment.