You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/lib/submitters.rb

273 lines
9.9 KiB

# frozen_string_literal: true
module Submitters
TRUE_VALUES = ['1', 'true', true].freeze
PRELOAD_ALL_PAGES_AMOUNT = 200
FIELD_NAME_WEIGHTS = {
'email' => 'A',
'phone' => 'B',
'name' => 'C',
'values' => 'D'
}.freeze
UnableToSendCode = Class.new(StandardError)
InvalidOtp = Class.new(StandardError)
MaliciousFileExtension = Class.new(StandardError)
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)
module_function
def search(current_user, submitters, keyword)
if Docuseal.fulltext_search?
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)
keyword = keyword.delete("\0")
return submitters.none if keyword.blank?
weight = FIELD_NAME_WEIGHTS[field_name]
return submitters.none if weight.blank?
query =
if keyword.match?(/\d/) && !keyword.match?(/\p{L}/)
number = keyword.gsub(/\D/, '')
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]
elsif keyword.match?(/[^\p{L}\d&@.-]/) || keyword.match?(/[.-]{2,}/)
terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq
if terms.size > 1
SearchEntries.build_weights_tsquery(terms, weight)
else
SearchEntries.build_weights_wildcard_tsquery(keyword, weight)
end
else
SearchEntries.build_weights_wildcard_tsquery(keyword, weight)
end
submitter_ids = SearchEntry.where(record_type: 'Submitter')
.where(account_id: current_user.account_id)
.where(*query)
.limit(500)
.pluck(:record_id)
submitters.where(id: submitter_ids.first(100))
end
def plain_search(submitters, keyword)
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
def select_attachments_for_download(submitter)
if AccountConfig.exists?(account_id: submitter.submission.account_id,
key: AccountConfig::COMBINE_PDF_RESULT_KEY,
value: true) &&
submitter.submission.submitters.all?(&:completed_at?) &&
submitter.submission.template_fields.none? { |f| f['type'] == 'verification' }
return [submitter.submission.combined_document_attachment || Submissions::EnsureCombinedGenerated.call(submitter)]
end
original_documents = submitter.submission.schema_documents.preload(:blob)
is_more_than_two_images = original_documents.many?(&:image?)
submitter.documents.preload(:blob).reject do |attachment|
is_more_than_two_images &&
original_documents.find { |a| a.uuid == (attachment.metadata['original_uuid'] || attachment.uuid) }&.image?
end
end
def create_attachment!(submitter, params)
blob =
if (file = params[:file])
extension = File.extname(file.original_filename).delete_prefix('.').downcase
if DANGEROUS_EXTENSIONS.include?(extension)
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
else
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
end
ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
record: submitter
)
end
def normalize_preferences(account, user, params)
preferences = {}
message_params = params['message'].presence || params.slice('subject', 'body').presence
if message_params.present?
email_message = EmailMessages.find_or_create_for_account_user(account, user,
message_params['subject'],
message_params['body'])
end
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')
preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa')
preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa')
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed')
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last')
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
preferences
end
def send_signature_requests(submitters, delay_seconds: nil)
submitters.each_with_index do |submitter, index|
next if submitter.email.blank?
next if submitter.declined_at?
next if submitter.preferences['send_email'] == false
if delay_seconds
SendSubmitterInvitationEmailJob.perform_in((delay_seconds + index).seconds, 'submitter_id' => submitter.id)
else
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
end
end
end
def current_submitter_order?(submitter)
submission = submitter.submission
submitter_items = submission.template_submitters || submission.template.submitters
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)
else
submitter_items.first(submitter_items.find_index { |e| e['uuid'] == submitter.uuid })
end
before_items.all? do |item|
submitter = submission.submitters.find { |e| e.uuid == item['uuid'] }
submitter.nil? || submitter.completed_at?
end
end
def build_document_filename(submitter, blob, filename_format)
return blob.filename.to_s if filename_format.blank?
filename = ReplaceEmailVariables.call(filename_format, submitter:)
filename = filename.gsub('{document.name}', blob.filename.base)
filename = filename.gsub(' - {submission.status}') do
if submitter.submission.submitters.all?(&:completed_at?)
status =
if submitter.submission.template_fields.any? { |f| f['type'] == 'signature' }
I18n.t(:signed)
else
I18n.t(:completed)
end
" - #{status}"
end
end
filename = filename.gsub(
'{submission.completed_at}',
I18n.l(submitter.completed_at.in_time_zone(submitter.account.timezone), format: :short)
)
"#{filename}.#{blob.filename.extension}"
end
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
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
end