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/submissions/create_from_pdf.rb

289 lines
9.6 KiB

# frozen_string_literal: true
module Submissions
module CreateFromPdf
PDF_CONTENT_TYPE = 'application/pdf'
BASE64_PDF_REGEXP = %r{\Adata:application/pdf(?:;[^,]+)?;base64,}i
URL_REGEXP = %r{\Ahttps?://}i
Error = Class.new(StandardError)
module_function
def call(user:, params:)
template = nil
attrs = params.to_h.with_indifferent_access
validate_attrs!(attrs)
template = build_template_with_documents(user, attrs)
submission = create_submission(template, user, attrs)
clone_documents_to_submission(template, submission)
enqueue_events(submission)
submission
rescue Templates::CreateAttachments::PdfEncrypted
raise Error, 'PDF encrypted'
rescue DownloadUtils::UnableToDownload => e
raise Error, e.message
rescue HexaPDF::Error, Pdfium::PdfiumError => e
raise Error, "Invalid PDF: #{e.message}"
ensure
archive_template(template) if template&.persisted? && template.archived_at.blank?
end
def validate_attrs!(attrs)
raise Error, 'documents are required' if attrs[:documents].blank?
raise Error, 'submitters are required' if attrs[:submitters].blank?
raise Error, 'template_ids are not supported by this endpoint yet' if attrs[:template_ids].present?
end
def build_template_with_documents(user, attrs)
template = build_template(user, attrs)
documents_attrs = sorted_documents_attrs(attrs[:documents])
documents_info = attach_documents(template, documents_attrs, attrs)
template.schema = build_schema(documents_info, documents_attrs)
template.submitters = build_template_submitters(attrs)
template.fields = build_fields(template, documents_info)
raise Error, 'PDF does not contain fields' if template.fields.blank?
template.tap(&:save!)
end
def build_template(user, attrs)
Template.create!(
account: user.account,
author: user,
folder: user.account.default_template_folder,
name: attrs[:name].presence || attrs.dig(:documents, 0, :name).presence || SecureRandom.uuid,
source: :api,
fields: [],
schema: [],
submitters: []
)
end
def build_schema(documents_info, documents_attrs)
documents_info.pluck(:attachment).map.with_index do |document, index|
{
attachment_uuid: document.uuid,
name: documents_attrs[index][:name].presence || document.filename.base
}
end
end
def create_submission(template, user, attrs)
Submissions.create_from_submitters(
template: template,
user: user,
source: :api,
with_template: false,
submitters_order: submitters_order(attrs),
submissions_attrs: [submission_attrs(attrs)]
).first
end
def submitters_order(attrs)
attrs[:order] || attrs[:submitters_order] || Submissions::DEFAULT_SUBMITTERS_ORDER
end
def enqueue_events(submission)
WebhookUrls.enqueue_events([submission], 'submission.created')
Submissions.send_signature_requests([submission])
SearchEntries.enqueue_reindex([submission])
end
def attach_documents(template, documents_attrs, params)
documents_attrs.map do |document_attrs|
file, text_tags = build_uploaded_file(document_attrs, remove_tags: remove_tags?(params))
attachment =
Templates::CreateAttachments.handle_pdf_or_image(template, file, file.read, params, extract_fields: true)
text_fields = text_tags.map do |tag|
field = tag[:field].deep_dup
field['areas'].each { |area| area['attachment_uuid'] = attachment.uuid }
field
end
{ attachment: attachment, text_fields: text_fields, attrs: document_attrs }
end
end
def remove_tags?(params)
!params[:remove_tags].in?([false, 'false', '0', 0])
end
def sorted_documents_attrs(documents_attrs)
documents_attrs.map(&:with_indifferent_access)
.sort_by.with_index { |item, index| item[:position].presence || index }
end
def build_uploaded_file(document_attrs, remove_tags: true)
name = document_attrs[:name].presence || 'document.pdf'
filename = name.ends_with?('.pdf') ? name : "#{name}.pdf"
data = read_document_data(document_attrs[:file])
text_tags = Templates::FindTextTags.call(data)
data = Templates::RemoveTextTags.call(data, text_tags) if remove_tags
tempfile = Tempfile.new(['docuseal-submission-pdf', '.pdf'])
tempfile.binmode
tempfile.write(data)
tempfile.rewind
file = ActionDispatch::Http::UploadedFile.new(
tempfile: tempfile,
filename: filename,
type: PDF_CONTENT_TYPE
)
[file, text_tags]
end
def read_document_data(value)
raise Error, 'documents[].file is required' if value.blank?
if value.to_s.match?(URL_REGEXP)
DownloadUtils.call(value, validate: true).body
else
Base64.strict_decode64(value.to_s.sub(BASE64_PDF_REGEXP, ''))
end
rescue ArgumentError
raise Error, 'documents[].file should be a PDF URL or base64 encoded PDF'
end
def build_template_submitters(attrs)
attrs[:submitters].map.with_index do |submitter_attrs, index|
name = submitter_attrs[:role].presence || submitter_attrs[:name].presence || default_submitter_name(index)
{
'name' => name,
'uuid' => SecureRandom.uuid,
'order' => submitter_attrs[:order]
}.compact
end
end
def default_submitter_name(index)
name = %w[First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth][index]
name ? "#{name} Party" : "Party #{index + 1}"
end
def build_fields(template, documents_info)
acro_fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents_info.pluck(:attachment))
acro_fields_index = acro_fields.group_by { |field| field['areas'].to_a.first&.dig('attachment_uuid') }
documents_info.flat_map do |info|
attachment = info[:attachment]
explicit_fields = Array.wrap(info[:attrs][:fields]).map do |field_attrs|
build_field(field_attrs.with_indifferent_access, attachment, template)
end
if explicit_fields.present?
explicit_fields
elsif info[:text_fields].present?
assign_tag_submitters(info[:text_fields], template)
else
acro_fields_index[attachment.uuid].to_a
end
end
end
def build_field(field_attrs, document, template)
raise Error, 'fields[].areas are required' if field_attrs[:areas].blank?
role = field_attrs[:role].presence
submitter = template.submitters.find { |item| item['name'].to_s.casecmp?(role.to_s) } ||
template.submitters.first
options = build_options(field_attrs[:options])
{
'uuid' => field_attrs[:uuid].presence || SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => field_attrs[:name].to_s,
'type' => field_attrs[:type].presence || 'text',
'required' => field_attrs.key?(:required) ? field_attrs[:required] : true,
'readonly' => field_attrs[:readonly],
'title' => field_attrs[:title],
'description' => field_attrs[:description],
'preferences' => field_attrs[:preferences] || {},
'validation' => field_attrs[:validation],
'options' => options,
'areas' => Array.wrap(field_attrs[:areas]).map do |area|
build_area(area.with_indifferent_access, document, options)
end
}.compact
end
def assign_tag_submitters(fields, template)
fields.map do |field|
role = field.delete('role').presence
submitter = template.submitters.find { |item| item['name'].to_s.casecmp?(role.to_s) } ||
template.submitters.first
field.merge('submitter_uuid' => submitter['uuid'])
end
end
def build_options(options)
Array.wrap(options).filter_map do |option|
if option.is_a?(Hash)
{
'value' => option[:value] || option['value'],
'uuid' => option[:uuid] || option['uuid'] || SecureRandom.uuid
}
elsif option.present?
{ 'value' => option, 'uuid' => SecureRandom.uuid }
end
end.presence
end
def build_area(area_attrs, document, options)
page = area_attrs[:page].to_i
option = options&.find { |item| item['value'] == area_attrs[:option] }
{
'x' => area_attrs[:x].to_f,
'y' => area_attrs[:y].to_f,
'w' => area_attrs[:w].to_f,
'h' => area_attrs[:h].to_f,
'cell_w' => area_attrs[:cell_w],
'option_uuid' => area_attrs[:option_uuid] || option&.dig('uuid'),
'attachment_uuid' => document.uuid,
'page' => page.positive? ? page - 1 : 0
}.compact
end
def submission_attrs(attrs)
attrs.slice(:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to,
:expire_at, :name, :message, :variables).merge(
submitters: attrs[:submitters]
)
end
def clone_documents_to_submission(template, submission)
template.schema_documents.each do |document|
new_document = submission.documents_attachments.create!(
uuid: document.uuid,
blob_id: document.blob_id
)
Templates::CloneAttachments.clone_document_preview_images_attachments(
document: document,
new_document: new_document
)
end
end
def archive_template(template)
template.update_column(:archived_at, Time.current)
rescue StandardError
nil
end
end
end