From 2e586e930e6e8c9fd16b3dbe6c6a624644ae5b5a Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 19 Oct 2026 19:03:26 +0300 Subject: [PATCH 01/20] adjust signature validation --- lib/verify_pdf_signature.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/verify_pdf_signature.rb b/lib/verify_pdf_signature.rb index 1404e9c4d..80739344c 100644 --- a/lib/verify_pdf_signature.rb +++ b/lib/verify_pdf_signature.rb @@ -180,8 +180,11 @@ module VerifyPdfSignature return false if byte_range.size != 4 || byte_range.any?(&:negative?) || byte_range[0].positive? return false if signature.signed_end > io.size + return false if byte_range[2] != byte_range[1] + (signature.contents.bytesize * 2) + 2 - byte_range[2] == byte_range[1] + (signature.contents.bytesize * 2) + 2 + io.seek(byte_range[1]) + + io.read(byte_range[2] - byte_range[1]).casecmp?("<#{signature.contents.unpack1('H*')}>") end def unsigned_changes?(document, io, signed_end) From b394854e9dc2605bf5a72f76f61319454d8cffea Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 20 Oct 2026 09:50:21 +0300 Subject: [PATCH 02/20] adjust send form --- app/javascript/elements/toggle_send_title.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/javascript/elements/toggle_send_title.js b/app/javascript/elements/toggle_send_title.js index 5d73e7706..4dc54aec7 100644 --- a/app/javascript/elements/toggle_send_title.js +++ b/app/javascript/elements/toggle_send_title.js @@ -12,7 +12,14 @@ export default class extends HTMLElement { toggle = (event) => { if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return - const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) + if (event?.type === 'focusin' && !this.isKeepSendUsed && this.isFocusFromName(event)) { + this.isKeepSendUsed = true + this.keepSendInput = event.target + } else if (event?.type === 'focusout' && event.target === this.keepSendInput) { + this.keepSendInput = null + } + + const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) && !this.keepSendInput?.matches(this.fieldSelector(field)) const isSend = (this.isChecked('send_email') && !isNameOnly('email')) || (this.isChecked('send_sms') && !isNameOnly('phone')) @@ -29,12 +36,17 @@ export default class extends HTMLElement { return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim()) } + isFocusFromName ({ target, relatedTarget }) { + return !!relatedTarget?.matches(this.fieldSelector('name')) && !!relatedTarget.value.trim() && + ['email', 'phone'].some((field) => target.matches(this.fieldSelector(field))) + } + fieldSelector (field) { return `[name$="[][${field}]"]` } get events () { - return ['change', 'input', 'linked-input.update'] + return ['change', 'input', 'linked-input.update', 'focusin', 'focusout'] } get form () { From 6d2b3bb654e06e15dad9c3092cffd6363a87f61d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 20 Oct 2026 20:15:37 +0300 Subject: [PATCH 03/20] adjust test webhook --- app/jobs/send_test_webhook_request_job.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/jobs/send_test_webhook_request_job.rb b/app/jobs/send_test_webhook_request_job.rb index 0116cdafb..6b14769d6 100644 --- a/app/jobs/send_test_webhook_request_job.rb +++ b/app/jobs/send_test_webhook_request_job.rb @@ -30,14 +30,16 @@ class SendTestWebhookRequestJob raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS) end - Faraday.post(webhook_url.url, - { - event_type: 'form.completed', - timestamp: Time.current.iso8601, - data: Submitters::SerializeForWebhook.call(submitter) - }.to_json, + body_json = { + event_type: 'form.completed', + timestamp: Time.current.iso8601, + data: Submitters::SerializeForWebhook.call(submitter) + }.to_json + + Faraday.post(webhook_url.url, body_json, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT, + 'X-Docuseal-Signature' => WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: body_json), **webhook_url.secret.to_h) end end From 85aa3d04515ed7fe07c468d51cf838f1128ead63 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 21 Oct 2026 09:35:38 +0300 Subject: [PATCH 04/20] mask email field value --- lib/replace_email_variables.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index f09548f95..4ac1ddec3 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -124,6 +124,8 @@ module ReplaceEmailVariables ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) end + elsif (mask = field.dig('preferences', 'mask').presence) + TextUtils.mask_value(value[field['uuid']].to_s, mask) else value[field&.dig('uuid')] end From 0647befb3ae1fde413a20ae750face3c70c7890a Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 21 Oct 2026 09:54:29 +0300 Subject: [PATCH 05/20] escape email variables --- lib/replace_email_variables.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index 4ac1ddec3..f4ce589aa 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -26,6 +26,8 @@ module ReplaceEmailVariables DOCUMENTS_LINKS = /\{+documents\.links\}+/i DOCUMENTS_LINK = /\{+documents\.link\}+/i + ESCAPED_BRACES = "\u{E000}\u{E001}" + EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil) module_function @@ -86,7 +88,11 @@ module ReplaceEmailVariables build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip) end - replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') } + text = replace(text, SENDER_EMAIL, html_escape:) do + submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') + end + + text.tr(ESCAPED_BRACES, '{}') end # rubocop:enable Metrics @@ -136,10 +142,12 @@ module ReplaceEmailVariables def replace(text, var, html_escape: false) text.gsub(var) do + value = yield(Regexp.last_match).to_s.tr('{}', ESCAPED_BRACES) + if html_escape - ERB::Util.html_escape(yield(Regexp.last_match)) + ERB::Util.html_escape(value) else - yield(Regexp.last_match) + value end end end From e9888c7466b84e7f0dee25a03a37ba1e025de5ba Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 21 Oct 2026 11:50:40 +0300 Subject: [PATCH 06/20] add validation --- lib/submitters/submit_values.rb | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 08920ea1c..20b135e7a 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -9,6 +9,8 @@ module Submitters PHONE_REGEXP = /[+\d()\s-]+/ NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze REQUIRED_FIELD_TYPES = %w[payment kba verification].freeze + CHOICE_FIELD_TYPES = %w[radio multiple select].freeze + BOOLEAN_VALUES = [true, false].freeze STRFTIME_MAP = { 'hour' => '%-k', @@ -55,6 +57,7 @@ module Submitters validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request) if params[:completed] == 'true' + validate_invite_submitters!(submitter) maybe_invite_via_field(submitter, request) SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) @@ -504,7 +507,7 @@ module Submitters submitter end - def validate_value!(_value, field, _params, submitter, _request) + def validate_value!(value, field, _params, submitter, _request) raise ValidationError, 'Missing field' unless field raise ValidationError, 'Invalid field' if field['submitter_uuid'] != submitter.uuid @@ -514,7 +517,34 @@ module Submitters raise ValidationError, 'Read-only field' end + raise ValidationError, 'Value is invalid' if field['type'] == 'checkbox' && !value.in?(BOOLEAN_VALUES) + + validate_choice_value!(value, field, submitter) if field['type'].in?(CHOICE_FIELD_TYPES) + true end + + def validate_choice_value!(value, field, submitter) + return if value.blank? + + raise ValidationError, 'Value is invalid' unless value.is_a?(field['type'] == 'multiple' ? Array : String) + + option_names = field['options'].to_a.map.with_index do |option, index| + option['value'].presence || + "#{I18n.t('option', locale: submitter.metadata.fetch('lang', submitter.account.locale))} #{index + 1}" + end + + raise ValidationError, 'Value is invalid' unless Array.wrap(value).all? { |item| option_names.include?(item) } + end + + def validate_invite_submitters!(submitter) + submission = submitter.submission + + is_pending_invite = (submission.template_submitters || submission.template.submitters).any? do |s| + s['invite_by_uuid'] == submitter.uuid && submission.submitters.none? { |e| e.uuid == s['uuid'] } + end + + raise ValidationError, 'Invite required' if is_pending_invite + end end end From 282b8cfa73fd007e6b00d115e418126f58bf2347 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 22 Oct 2026 11:02:24 +0300 Subject: [PATCH 07/20] fix search --- lib/search_entries.rb | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/search_entries.rb b/lib/search_entries.rb index f199872fa..8c0fd9f41 100644 --- a/lib/search_entries.rb +++ b/lib/search_entries.rb @@ -3,6 +3,8 @@ module SearchEntries MAX_VALUE_LENGTH = 100 + MUTEX = Mutex.new + UUID_REGEXP = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i module_function @@ -70,10 +72,10 @@ module SearchEntries arel else - "tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*')::tsquery" + "tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*')::tsquery" end - [sql, { keyword: }] + [sql, { keyword:, dictionary: default_dictionary }] end end @@ -83,7 +85,7 @@ module SearchEntries Arel.sql("ngram @@ (quote_literal(:term#{terms.size - 1}) || ':' || :weight)::tsquery") else Arel.sql(<<~SQL.squish) - (quote_literal(coalesce((ts_lexize('english_stem', :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery + (quote_literal(coalesce((ts_lexize(:dictionary, :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery SQL end @@ -91,7 +93,7 @@ module SearchEntries index = terms.index(term) arel = Arel.sql(<<~SQL.squish) - (quote_literal(coalesce((ts_lexize('english_stem', :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery + (quote_literal(coalesce((ts_lexize(:dictionary, :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery SQL acc ? Arel::Nodes::InfixOperation.new('&&', arel, acc) : arel @@ -109,7 +111,8 @@ module SearchEntries ) end - [query.to_sql, terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:)] + [query.to_sql, + terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:, dictionary: default_dictionary)] end def build_weights_wildcard_tsquery(keyword, weight) @@ -122,11 +125,11 @@ module SearchEntries SQL else <<~SQL.squish - tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*' || :weight)::tsquery + tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*' || :weight)::tsquery SQL end - [sql, { keyword:, weight: }] + [sql, { keyword:, weight:, dictionary: default_dictionary }] end def index_submitter(submitter) @@ -256,4 +259,14 @@ module SearchEntries ngrams.uniq { |e| e.sub(/':[\d,]/, "':1") }.join(' ') end + + def default_dictionary + @default_dictionary || MUTEX.synchronize do + @default_dictionary ||= SearchEntry.connection.select_value(<<~SQL.squish) + SELECT mapdict::regdictionary::text FROM pg_ts_config_map + WHERE mapcfg = get_current_ts_config() AND maptokentype = 1 + ORDER BY mapseqno LIMIT 1 + SQL + end + end end From 88c99a2b35913d2820486e888ff2b37460d65db6 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 22 Oct 2026 14:48:03 +0300 Subject: [PATCH 08/20] fix turbo morph --- app/views/layouts/application.html.erb | 6 +++--- app/views/shared/_navbar.html.erb | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cf5afe1a8..a2beb0461 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -33,10 +33,10 @@ <% end %> <%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %> - <% if flash.present? %><%= render 'shared/flash' %><% end %> -
+
<%= yield %> -
+ + <% if flash.present? %><%= render 'shared/flash' %><% end %> <%= render 'shared/body_scripts' %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index f44a3dac3..36e165dc8 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -1,5 +1,5 @@ <%= render 'shared/navbar_warning' %> - + From 78edd87cc173e0b9785b98a9b9c7f9bbd97f1eaa Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 22 Oct 2026 15:01:54 +0300 Subject: [PATCH 09/20] add main tag --- app/views/layouts/form.html.erb | 4 +++- app/views/layouts/plain.html.erb | 6 ++++-- app/views/start_form/completed.html.erb | 4 ++-- app/views/start_form/email_verification.html.erb | 4 ++-- app/views/start_form/error.html.erb | 4 ++-- app/views/start_form/private.html.erb | 4 ++-- app/views/start_form/show.html.erb | 4 ++-- app/views/submissions/show.html.erb | 4 ++-- app/views/submit_form/completed.html.erb | 4 ++-- app/views/submit_form/email_2fa.html.erb | 4 ++-- app/views/submit_form/show.html.erb | 4 ++-- 11 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/views/layouts/form.html.erb b/app/views/layouts/form.html.erb index 400b0bd9a..f74a29055 100644 --- a/app/views/layouts/form.html.erb +++ b/app/views/layouts/form.html.erb @@ -13,6 +13,8 @@ <%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %> - <%= yield %> +
+ <%= yield %> +
diff --git a/app/views/layouts/plain.html.erb b/app/views/layouts/plain.html.erb index f23f9b267..6c2f5fe8b 100644 --- a/app/views/layouts/plain.html.erb +++ b/app/views/layouts/plain.html.erb @@ -13,8 +13,10 @@ <%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %> - <% if flash.present? %><%= render 'shared/flash' %><% end %> - <%= yield %> +
+ <%= yield %> +
+ <% if flash.present? %><%= render 'shared/flash' %><% end %> diff --git a/app/views/start_form/completed.html.erb b/app/views/start_form/completed.html.erb index bb576a831..c432e273d 100644 --- a/app/views/start_form/completed.html.erb +++ b/app/views/start_form/completed.html.erb @@ -1,4 +1,4 @@ -
+
@@ -32,5 +32,5 @@ <% end %>
-
+ <%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> diff --git a/app/views/start_form/email_verification.html.erb b/app/views/start_form/email_verification.html.erb index b1a8410eb..383d8c9f6 100644 --- a/app/views/start_form/email_verification.html.erb +++ b/app/views/start_form/email_verification.html.erb @@ -2,7 +2,7 @@ <% I18n.with_locale(@template.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% end %> -
+
@@ -55,4 +55,4 @@ <%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %> <% end %>
-
+ diff --git a/app/views/start_form/error.html.erb b/app/views/start_form/error.html.erb index 3c9f56dd9..289abe7ac 100644 --- a/app/views/start_form/error.html.erb +++ b/app/views/start_form/error.html.erb @@ -2,7 +2,7 @@ <% I18n.with_locale(@template.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% end %> -
+
@@ -28,4 +28,4 @@ <%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %>
-
+ diff --git a/app/views/start_form/private.html.erb b/app/views/start_form/private.html.erb index d0f645800..d022342fd 100644 --- a/app/views/start_form/private.html.erb +++ b/app/views/start_form/private.html.erb @@ -2,7 +2,7 @@ <% I18n.with_locale(@template.account.locale) do %> <% content_for(:html_description, t('share_link_is_currently_disabled')) %> <% end %> -
+
<%= render 'start_form/banner' %>

@@ -27,5 +27,5 @@ <%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %> <% end %> -

+ <%= render 'shared/attribution', link_path: '/start', account: @template.account %> diff --git a/app/views/start_form/show.html.erb b/app/views/start_form/show.html.erb index 097ff38ce..dfe336914 100644 --- a/app/views/start_form/show.html.erb +++ b/app/views/start_form/show.html.erb @@ -2,7 +2,7 @@ <% I18n.with_locale(@template.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% end %> -
+
@@ -66,4 +66,4 @@ <%= render 'shared/attribution', link_path: '/start', account: @template.account %> <%= render 'start_form/policy', account: @template.account %>
-
+ diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index e749af450..0599fc36b 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -11,7 +11,7 @@ <% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %> <% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %> <% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %> -
+
<% unless native_tab == 'recipients' %>
<% unless turbo_native_app? %> @@ -394,7 +394,7 @@ <% end %> -
+ <% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %> <%= render 'scripts/autosize_field' %> <% end %> diff --git a/app/views/submit_form/completed.html.erb b/app/views/submit_form/completed.html.erb index 7c7693814..2b5855903 100644 --- a/app/views/submit_form/completed.html.erb +++ b/app/views/submit_form/completed.html.erb @@ -1,5 +1,5 @@ <% completed_at = @submitter.completed_at || @submitter.submission.completed_at %> -
+
@@ -51,5 +51,5 @@ <% end %>
-
+ <%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> diff --git a/app/views/submit_form/email_2fa.html.erb b/app/views/submit_form/email_2fa.html.erb index 6bb4a584b..878bad01a 100644 --- a/app/views/submit_form/email_2fa.html.erb +++ b/app/views/submit_form/email_2fa.html.erb @@ -3,7 +3,7 @@ <% I18n.with_locale(@submitter.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %> <% end %> -
+
@@ -70,4 +70,4 @@
<%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %>
<% end %>
-
+ diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index b02a8a273..9e62abdbd 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -14,7 +14,7 @@ <% delegate_modal_id = nil %> <% banner_html = turbo_native_app? ? '' : local_assigns[:banner_html] %>
-
+
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %> @@ -172,7 +172,7 @@ <% end %> <% end %>
-
+