Files
docuseal/lib/submitters.rb
T

311 lines
12 KiB
Ruby
Raw Normal View History

2023-06-26 00:46:01 +03:00
# frozen_string_literal: true
module Submitters
2023-11-23 20:03:52 +02:00
TRUE_VALUES = ['1', 'true', true].freeze
2025-06-05 13:16:55 +03:00
FIELD_NAME_WEIGHTS = {
'email' => 'A',
'phone' => 'B',
'name' => 'C',
'values' => 'D'
}.freeze
2025-07-11 09:02:18 +03:00
UnableToSendCode = Class.new(StandardError)
InvalidOtp = Class.new(StandardError)
2025-10-30 08:34:30 +02:00
MaliciousFileExtension = Class.new(StandardError)
2026-04-25 12:39:26 +03:00
ParamsError = Class.new(StandardError)
2025-10-30 08:34:30 +02:00
DANGEROUS_EXTENSIONS = Set.new(%w[
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
hta cpl jar app deb rpm dmg pkg mpkg dll so dylib sys
inf reg ps1 psm1 psd1 ps1xml psc1 pssc bat cmd vb vba
sh bash zsh fish run out bin elf gadget workflow lnk scf
url desktop application action workflow apk ipa xap appx
appxbundle msix msixbundle diagcab diagpkg cpl msc ocx
drv scr ins isp mst paf prf shb shs slk ws wsc inf1 inf2
].freeze)
2025-07-11 09:02:18 +03:00
2026-04-10 11:07:19 +03:00
FILES_TTL = 5.minutes
2023-06-26 00:46:01 +03:00
module_function
2025-06-05 13:16:55 +03:00
def search(current_user, submitters, keyword)
2025-06-21 22:52:10 +03:00
if Docuseal.fulltext_search?
2025-06-05 13:16:55 +03:00
fulltext_search(current_user, submitters, keyword)
else
plain_search(submitters, keyword)
end
end
def fulltext_search(current_user, submitters, keyword)
return submitters if keyword.blank?
submitters.where(
id: SearchEntry.where(record_type: 'Submitter')
.where(account_id: current_user.account_id)
.where(*SearchEntries.build_tsquery(keyword))
.select(:record_id)
)
end
def fulltext_search_field(current_user, submitters, keyword, field_name)
2025-06-07 09:50:04 +03:00
keyword = keyword.delete("\0")
2025-06-18 23:52:46 +03:00
return submitters.none if keyword.blank?
2025-06-05 13:16:55 +03:00
weight = FIELD_NAME_WEIGHTS[field_name]
2025-06-18 23:52:46 +03:00
return submitters.none if weight.blank?
2025-06-05 13:16:55 +03:00
query =
if keyword.match?(/\d/) && !keyword.match?(/\p{L}/)
number = keyword.gsub(/\D/, '')
2025-06-09 09:55:30 +03:00
sql =
if number.length <= 2
"ngram @@ ((quote_literal(?) || ':' || ?)::tsquery || (quote_literal(?) || ':' || ?)::tsquery)"
else
"tsvector @@ ((quote_literal(?) || ':*' || ?)::tsquery || (quote_literal(?) || ':*' || ?)::tsquery)"
end
[sql, number, weight, number.length > 1 ? number.delete_prefix('0') : number, weight]
2025-10-04 18:33:37 +03:00
elsif keyword.match?(/[^\p{L}\d&@.-]/) || keyword.match?(/[.-]{2,}/)
2025-06-05 13:16:55 +03:00
terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq
if terms.size > 1
SearchEntries.build_weights_tsquery(terms, weight)
else
2025-06-09 09:55:30 +03:00
SearchEntries.build_weights_wildcard_tsquery(keyword, weight)
2025-06-05 13:16:55 +03:00
end
else
2025-06-09 09:55:30 +03:00
SearchEntries.build_weights_wildcard_tsquery(keyword, weight)
2025-06-05 13:16:55 +03:00
end
2025-06-19 15:43:13 +03:00
submitter_ids = SearchEntry.where(record_type: 'Submitter')
.where(account_id: current_user.account_id)
.where(*query)
2026-07-10 10:32:04 +03:00
.limit(keyword.strip.length > 2 ? 500 : 5000)
2025-06-19 15:43:13 +03:00
.pluck(:record_id)
submitters.where(id: submitter_ids.first(100))
2025-06-05 13:16:55 +03:00
end
def plain_search(submitters, keyword)
2023-09-20 02:00:00 +03:00
return submitters if keyword.blank?
term = "%#{keyword.downcase}%"
arel_table = Submitter.arel_table
arel = arel_table[:email].lower.matches(term)
.or(arel_table[:phone].matches(term))
.or(arel_table[:name].lower.matches(term))
submitters.where(arel)
end
2023-06-26 00:46:01 +03:00
def select_attachments_for_download(submitter)
2026-07-09 11:40:55 +03:00
if AccountConfig.exists?(account_id: submitter.account_id,
2024-08-23 23:46:22 +03:00
key: AccountConfig::COMBINE_PDF_RESULT_KEY,
2025-09-15 10:53:05 +03:00
value: true) &&
2026-07-01 21:50:12 +03:00
submitter.submission.completed_at? &&
2025-12-12 20:34:53 +02:00
submitter.submission.template_fields.none? { |f| f['type'] == 'verification' }
2025-09-15 10:53:05 +03:00
return [submitter.submission.combined_document_attachment || Submissions::EnsureCombinedGenerated.call(submitter)]
2024-08-23 23:46:22 +03:00
end
2025-05-31 15:15:25 +03:00
original_documents = submitter.submission.schema_documents.preload(:blob)
2025-10-04 18:33:37 +03:00
is_more_than_two_images = original_documents.many?(&:image?)
2023-06-26 00:46:01 +03:00
submitter.documents.preload(:blob).reject do |attachment|
2024-02-25 16:25:04 +02:00
is_more_than_two_images &&
original_documents.find { |a| a.uuid == (attachment.metadata['original_uuid'] || attachment.uuid) }&.image?
2023-06-26 00:46:01 +03:00
end
end
2023-08-12 12:34:02 +03:00
2026-05-30 22:37:39 +03:00
def create_attachment!(submitter, file, metadata: {})
2026-05-21 15:38:16 +03:00
raise ParamsError, 'file param is missing' if file.blank?
2025-10-30 08:34:30 +02:00
2026-05-21 15:38:16 +03:00
extension = File.extname(file.original_filename).delete_prefix('.').downcase
2025-10-30 08:34:30 +02:00
2026-05-21 15:38:16 +03:00
if DANGEROUS_EXTENSIONS.include?(extension)
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
2023-08-12 12:34:02 +03:00
2026-05-21 15:38:16 +03:00
blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open,
filename: file.original_filename,
2026-05-30 22:37:39 +03:00
content_type: file.content_type,
metadata:)
2026-05-21 15:38:16 +03:00
ActiveStorage::Attachment.create!(blob:, name: 'attachments', record: submitter)
2023-08-12 12:34:02 +03:00
end
2023-09-03 14:05:31 +03:00
2023-11-23 20:03:52 +02:00
def normalize_preferences(account, user, params)
preferences = {}
2023-09-03 14:05:31 +03:00
2023-11-23 20:03:52 +02:00
message_params = params['message'].presence || params.slice('subject', 'body').presence
2023-09-03 14:05:31 +03:00
2023-11-23 20:03:52 +02:00
if message_params.present?
email_message = EmailMessages.find_or_create_for_account_user(account, user,
message_params['subject'],
message_params['body'])
2023-09-03 14:05:31 +03:00
end
2023-11-23 20:03:52 +02:00
preferences['email_message_uuid'] = email_message.uuid if email_message
preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email')
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
2025-06-28 10:37:10 +03:00
preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa')
2025-12-09 11:46:57 +02:00
preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa')
2024-01-10 02:06:31 +02:00
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed')
2024-03-24 14:09:28 +02:00
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
2024-04-13 15:33:05 +03:00
preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last')
2024-01-14 12:11:13 +02:00
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
2023-11-23 20:03:52 +02:00
preferences
2023-09-03 14:05:31 +03:00
end
2024-11-11 19:19:06 +02:00
def send_signature_requests(submitters, delay_seconds: nil)
submitters.each_with_index do |submitter, index|
2023-11-23 20:03:52 +02:00
next if submitter.email.blank?
2025-04-27 21:43:31 +03:00
next if submitter.declined_at?
2023-11-23 20:03:52 +02:00
next if submitter.preferences['send_email'] == false
2024-11-11 19:19:06 +02:00
if delay_seconds
SendSubmitterInvitationEmailJob.perform_in((delay_seconds + index).seconds, 'submitter_id' => submitter.id)
else
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
end
2023-11-23 20:03:52 +02:00
end
end
2024-09-26 15:47:18 +03:00
2026-07-04 15:47:40 +03:00
# rubocop:disable Metrics
2024-09-26 15:47:18 +03:00
def current_submitter_order?(submitter)
2025-07-29 16:15:55 +03:00
submission = submitter.submission
2024-09-26 15:47:18 +03:00
2025-07-29 16:15:55 +03:00
submitter_items = submission.template_submitters || submission.template.submitters
2024-09-26 15:47:18 +03:00
2025-07-29 16:15:55 +03:00
before_items =
if submitter_items.any? { |s| s['order'] }
submitter_groups = submitter_items.group_by.with_index { |s, index| s['order'] || index }.sort_by(&:first)
current_group_index = submitter_groups.find_index { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }
submitter_groups.first(current_group_index).flat_map(&:last)
2026-07-04 15:47:40 +03:00
elsif submitter.viewer?
current_index = submitter_items.find_index { |e| e['uuid'] == submitter.uuid }
preceding_submitter_index = submitter_items[0...current_index].rindex do |e|
!submission.submitters.find { |s| s.uuid == e['uuid'] }&.viewer?
end
submitter_items.first(preceding_submitter_index || 0)
2025-07-29 16:15:55 +03:00
else
submitter_items.first(submitter_items.find_index { |e| e['uuid'] == submitter.uuid })
end
2025-08-04 14:20:42 +03:00
before_items.all? do |item|
submitter = submission.submitters.find { |e| e.uuid == item['uuid'] }
2026-07-04 15:47:40 +03:00
submitter.nil? || submitter.viewer? || submitter.completed_at?
2025-08-04 14:20:42 +03:00
end
2024-09-26 15:47:18 +03:00
end
2026-07-04 15:47:40 +03:00
# rubocop:enable Metrics
2024-11-20 21:48:12 +02:00
def build_document_filename(submitter, blob, filename_format)
2024-11-21 23:18:27 +02:00
return blob.filename.to_s if filename_format.blank?
2024-11-20 21:48:12 +02:00
filename = ReplaceEmailVariables.call(filename_format, submitter:)
filename = filename.gsub('{document.name}', blob.filename.base)
2025-01-19 16:01:33 +02:00
filename = filename.gsub(' - {submission.status}') do
2026-07-01 21:50:12 +03:00
if submitter.submission.completed_at?
2025-01-19 16:01:33 +02:00
status =
if submitter.submission.template_fields.any? { |f| f['type'] == 'signature' }
I18n.t(:signed)
else
I18n.t(:completed)
end
" - #{status}"
end
end
2024-11-20 21:48:12 +02:00
2026-07-09 11:40:55 +03:00
filename = filename.gsub('{submission.completed_at}') do
completed_at = submitter.submission.completed_at ||
submitter.submission.submitters.select(&:completed_at).max_by(&:completed_at).completed_at
I18n.l(completed_at.in_time_zone(submitter.account.timezone), format: :short)
end
2024-11-20 21:48:12 +02:00
"#{filename}.#{blob.filename.extension}"
end
2025-07-11 09:02:18 +03:00
def send_shared_link_email_verification_code(submitter, request:)
RateLimit.call("send-otp-code-#{request.remote_ip}", limit: 2, ttl: 45.seconds, enabled: true)
TemplateMailer.otp_verification_email(submitter.submission.template, email: submitter.email).deliver_later!
rescue RateLimit::LimitApproached
Rollbar.warning("Limit verification code for template: #{submitter.submission.template.id}") if defined?(Rollbar)
raise UnableToSendCode, I18n.t('too_many_attempts')
end
def verify_link_otp!(otp, submitter)
return false if otp.blank?
RateLimit.call("verify-2fa-code-#{Digest::MD5.base64digest(submitter.email)}",
limit: 2, ttl: 45.seconds, enabled: true)
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
raise InvalidOtp, I18n.t(:invalid_code) unless EmailVerificationCodes.verify(otp, link_2fa_key)
true
end
2025-11-21 12:14:56 +02:00
2026-04-10 11:07:19 +03:00
def build_document_urls(submitter, ttl: FILES_TTL)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
2026-04-10 12:08:22 +03:00
select_attachments_for_download(submitter).map do |attachment|
2026-04-10 11:07:19 +03:00
ActiveStorage::Blob.proxy_path(
attachment.blob,
expires_at: ttl.from_now.to_i,
2026-04-10 12:08:22 +03:00
filename: build_document_filename(submitter, attachment.blob, filename_format)
2026-04-10 11:07:19 +03:00
)
end
end
def build_combined_url(submitter, ttl: FILES_TTL)
2026-07-01 21:50:12 +03:00
return unless submitter.submission.completed_at?
2026-07-09 11:40:55 +03:00
return if submitter.submission.submitters.completed.order(:completed_at).last != submitter
2026-04-10 11:07:19 +03:00
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_path(
attachment.blob,
expires_at: ttl.from_now.to_i,
2026-04-10 12:08:22 +03:00
filename: build_document_filename(submitter, attachment.blob, filename_format)
2026-04-10 11:07:19 +03:00
)
end
2025-11-21 12:14:56 +02:00
def populate_completed_is_first
Account.find_each do |account|
submissions_index = {}
CompletedSubmitter.where(account_id: account.id).order(:account_id, :completed_at).each do |cs|
submissions_index[cs.submission_id] ||= cs.submitter_id
cs.update_columns(is_first: submissions_index[cs.submission_id] == cs.submitter_id)
rescue ActiveRecord::RecordNotUnique
CompletedSubmitter.where(submission_id: cs.submission_id).update_all(is_first: false)
cs.update_columns(is_first: submissions_index[cs.submission_id] == cs.submitter_id)
end
end
end
2023-06-26 00:46:01 +03:00
end