process password protected pdf files
This commit is contained in:
committed by
Pete Matsyburka
parent
a573545ead
commit
e864e8a47f
@@ -22,6 +22,8 @@ module Api
|
||||
}
|
||||
)
|
||||
}
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,12 +10,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
def create
|
||||
url_params = create_file_params_from_url if params[:url].present?
|
||||
|
||||
@template.account = current_account
|
||||
@template.author = current_user
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||
|
||||
@template.save!
|
||||
save_template!(@template, url_params)
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||
|
||||
@@ -24,6 +19,8 @@ class TemplatesUploadsController < ApplicationController
|
||||
@template.update!(schema:)
|
||||
|
||||
redirect_to edit_template_path(@template)
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
||||
rescue StandardError => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
@@ -32,6 +29,17 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def save_template!(template, url_params)
|
||||
template.account = current_account
|
||||
template.author = current_user
|
||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||
|
||||
template.save!
|
||||
|
||||
template
|
||||
end
|
||||
|
||||
def create_file_params_from_url
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
|
||||
@@ -19,6 +19,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||
import SignatureForm from './elements/signature_form'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
import PromptPassword from './elements/prompt_password'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -51,6 +52,7 @@ window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||
window.customElements.define('signature-form', SignatureForm)
|
||||
window.customElements.define('submit-form', SubmitForm)
|
||||
window.customElements.define('prompt-password', PromptPassword)
|
||||
|
||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||
document.addEventListener('turbo:submit-end', async (event) => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const input = document.createElement('input')
|
||||
|
||||
input.type = 'hidden'
|
||||
input.name = 'password'
|
||||
input.value = prompt('Enter PDF password')
|
||||
|
||||
this.form.append(input)
|
||||
|
||||
this.form.requestSubmit()
|
||||
|
||||
this.remove()
|
||||
}
|
||||
|
||||
get form () {
|
||||
return this.closest('form')
|
||||
}
|
||||
}
|
||||
@@ -123,9 +123,30 @@ export default {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ blobs }),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(resp => resp.json()).then((data) => {
|
||||
this.$emit('success', data)
|
||||
this.$refs.input.value = ''
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
resp.json().then((data) => {
|
||||
this.$emit('success', data)
|
||||
this.$refs.input.value = ''
|
||||
})
|
||||
} else if (resp.status === 422) {
|
||||
resp.json().then((data) => {
|
||||
if (data.error === 'PDF encrypted') {
|
||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
this.$emit('success', await resp.json())
|
||||
this.$refs.input.value = ''
|
||||
} else {
|
||||
alert('Wrong password')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isProcessing = false
|
||||
})
|
||||
@@ -133,9 +154,33 @@ export default {
|
||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||
method: 'POST',
|
||||
body: new FormData(this.$refs.form)
|
||||
}).then(resp => resp.json()).then((data) => {
|
||||
this.$emit('success', data)
|
||||
this.$refs.input.value = ''
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
resp.json().then((data) => {
|
||||
this.$emit('success', data)
|
||||
this.$refs.input.value = ''
|
||||
})
|
||||
} else if (resp.status === 422) {
|
||||
resp.json().then((data) => {
|
||||
if (data.error === 'PDF encrypted') {
|
||||
const formData = new FormData(this.$refs.form)
|
||||
|
||||
formData.append('password', prompt('Enter PDF password'))
|
||||
|
||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
this.$emit('success', await resp.json())
|
||||
this.$refs.input.value = ''
|
||||
} else {
|
||||
alert('Wrong password')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
|
||||
<%= form_for '', url: templates_upload_path, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
|
||||
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
|
||||
<input type="hidden" name="form_id" value="<%= form_id %>">
|
||||
<button type="submit" class="hidden"></button>
|
||||
<file-dropzone data-submit-on-upload="true" class="w-full">
|
||||
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= form_for '', url: templates_upload_path, method: :post, class: 'inline', html: { enctype: 'multipart/form-data' } do %>
|
||||
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'inline', html: { enctype: 'multipart/form-data' } do %>
|
||||
<button type="submit" class="btn btn-ghost text-base" onclick="[event.preventDefault(), window.upload_template.click()]">
|
||||
<span class="enabled">
|
||||
<span class="flex items-center justify-center space-x-2">
|
||||
@@ -13,6 +13,7 @@
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<input type="hidden" name="form_id" value="<%= form_id %>">
|
||||
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
|
||||
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PdfUtils
|
||||
module_function
|
||||
|
||||
def encrypted?(data, password: nil)
|
||||
HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
|
||||
|
||||
false
|
||||
rescue HexaPDF::EncryptionError
|
||||
true
|
||||
end
|
||||
|
||||
def decrypt(data, password)
|
||||
encrypted_doc = HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
|
||||
|
||||
decrypted_doc = HexaPDF::Document.new
|
||||
|
||||
encrypted_doc.pages.each do |page|
|
||||
decrypted_doc.pages << decrypted_doc.import(page)
|
||||
end
|
||||
|
||||
decrypted_io = StringIO.new
|
||||
|
||||
decrypted_doc.write(decrypted_io, validate: false)
|
||||
|
||||
decrypted_io.tap(&:rewind).read
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ module Templates
|
||||
PDF_CONTENT_TYPE = 'application/pdf'
|
||||
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
|
||||
InvalidFileType = Class.new(StandardError)
|
||||
PdfEncrypted = Class.new(StandardError)
|
||||
|
||||
module_function
|
||||
|
||||
@@ -19,6 +20,15 @@ module Templates
|
||||
document = template.documents.create!(blob:)
|
||||
|
||||
if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil?
|
||||
data = maybe_decrypt_pdf_or_raise(document_data, params)
|
||||
|
||||
if data != document_data
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(new_data), filename: blob.filename
|
||||
)
|
||||
document_data = data
|
||||
end
|
||||
|
||||
annotations =
|
||||
document_data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(document_data)
|
||||
|
||||
@@ -41,6 +51,8 @@ module Templates
|
||||
data = file.read
|
||||
|
||||
if file.content_type == PDF_CONTENT_TYPE
|
||||
data = maybe_decrypt_pdf_or_raise(data, params)
|
||||
|
||||
annotations = data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(data)
|
||||
metadata = { 'identified' => true, 'analyzed' => true,
|
||||
'sha256' => Base64.urlsafe_encode64(Digest::SHA256.digest(data)),
|
||||
@@ -56,6 +68,16 @@ module Templates
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_decrypt_pdf_or_raise(data, params)
|
||||
if data.size < ANNOTATIONS_SIZE_LIMIT && PdfUtils.encrypted?(data)
|
||||
PdfUtils.decrypt(data, params[:password])
|
||||
else
|
||||
data
|
||||
end
|
||||
rescue HexaPDF::EncryptionError
|
||||
raise PdfEncrypted
|
||||
end
|
||||
|
||||
def handle_file_types(_document_data, blob)
|
||||
raise InvalidFileType, blob.content_type
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user