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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
url_params = create_file_params_from_url if params[:url].present?
|
url_params = create_file_params_from_url if params[:url].present?
|
||||||
|
|
||||||
@template.account = current_account
|
save_template!(@template, url_params)
|
||||||
@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!
|
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||||
|
|
||||||
@@ -24,6 +19,8 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
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
|
rescue StandardError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -32,6 +29,17 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
private
|
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
|
def create_file_params_from_url
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
|||||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
|
import PromptPassword from './elements/prompt_password'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
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('folder-autocomplete', FolderAutocomplete)
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
window.customElements.define('submit-form', SubmitForm)
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
|
window.customElements.define('prompt-password', PromptPassword)
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
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',
|
method: 'POST',
|
||||||
body: JSON.stringify({ blobs }),
|
body: JSON.stringify({ blobs }),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then((resp) => {
|
||||||
this.$emit('success', data)
|
if (resp.ok) {
|
||||||
this.$refs.input.value = ''
|
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(() => {
|
}).finally(() => {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
})
|
})
|
||||||
@@ -133,9 +154,33 @@ export default {
|
|||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then((resp) => {
|
||||||
this.$emit('success', data)
|
if (resp.ok) {
|
||||||
this.$refs.input.value = ''
|
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(() => {
|
}).finally(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% 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)) %>
|
<% 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>
|
<button type="submit" class="hidden"></button>
|
||||||
<file-dropzone data-submit-on-upload="true" class="w-full">
|
<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">
|
<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()]">
|
<button type="submit" class="btn btn-ghost text-base" onclick="[event.preventDefault(), window.upload_template.click()]">
|
||||||
<span class="enabled">
|
<span class="enabled">
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<span class="flex items-center justify-center space-x-2">
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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 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] %>">
|
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
|
||||||
<% end %>
|
<% 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'
|
PDF_CONTENT_TYPE = 'application/pdf'
|
||||||
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
|
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
|
||||||
InvalidFileType = Class.new(StandardError)
|
InvalidFileType = Class.new(StandardError)
|
||||||
|
PdfEncrypted = Class.new(StandardError)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -19,6 +20,15 @@ module Templates
|
|||||||
document = template.documents.create!(blob:)
|
document = template.documents.create!(blob:)
|
||||||
|
|
||||||
if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil?
|
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 =
|
annotations =
|
||||||
document_data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(document_data)
|
document_data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(document_data)
|
||||||
|
|
||||||
@@ -41,6 +51,8 @@ module Templates
|
|||||||
data = file.read
|
data = file.read
|
||||||
|
|
||||||
if file.content_type == PDF_CONTENT_TYPE
|
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)
|
annotations = data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(data)
|
||||||
metadata = { 'identified' => true, 'analyzed' => true,
|
metadata = { 'identified' => true, 'analyzed' => true,
|
||||||
'sha256' => Base64.urlsafe_encode64(Digest::SHA256.digest(data)),
|
'sha256' => Base64.urlsafe_encode64(Digest::SHA256.digest(data)),
|
||||||
@@ -56,6 +68,16 @@ module Templates
|
|||||||
end
|
end
|
||||||
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)
|
def handle_file_types(_document_data, blob)
|
||||||
raise InvalidFileType, blob.content_type
|
raise InvalidFileType, blob.content_type
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user