Merge from docusealco/wip

pull/604/merge 3.0.2
Alex Turchyn 3 weeks ago committed by GitHub
commit 1f89accac3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -48,7 +48,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app
RUN apk add --no-cache libpq vips redis onnxruntime
RUN apk add --no-cache libpq vips redis onnxruntime && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal

@ -21,7 +21,6 @@ gem 'faraday'
gem 'faraday-follow_redirects'
gem 'google-cloud-storage', require: false
gem 'hexapdf'
gem 'image_processing'
gem 'jwt', require: false
gem 'lograge'
gem 'numo-narray-alt', require: false

@ -264,9 +264,6 @@ GEM
strscan (>= 3.1.2)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
@ -274,7 +271,7 @@ GEM
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.19.5)
json (2.19.7)
jwt (3.2.0)
base64
language_server-protocol (3.17.0.5)
@ -308,8 +305,6 @@ GEM
marcel (1.1.0)
matrix (0.4.3)
method_source (1.1.0)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
minitest (6.0.6)
drb (~> 2.0)
@ -434,7 +429,7 @@ GEM
erb
psych (>= 4.0.0)
tsort
redis-client (0.28.0)
redis-client (0.29.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
@ -516,12 +511,12 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
sidekiq (8.1.2)
sidekiq (8.1.6)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
rack (>= 3.2.0)
redis-client (>= 0.26.0)
redis-client (>= 0.29.0)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
@ -624,7 +619,6 @@ DEPENDENCIES
foreman
google-cloud-storage
hexapdf
image_processing
jwt
letter_opener_web
lograge

@ -11,8 +11,6 @@ module Api
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(@submitter)
Rollbar.error("Can't upload: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
@ -33,9 +31,11 @@ module Api
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content
end
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end
attachment = Submitters.create_attachment!(@submitter, file)
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)

@ -9,20 +9,7 @@ module Api
templates = paginate(templates.preload(:author, folder: :parent_folder))
schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
preview_image_attachments =
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] })
.where(record_id: schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
expires_at = Accounts.link_expires_at(current_account)
@ -30,6 +17,7 @@ module Api
data: templates.map do |t|
Templates::SerializeForApi.call(t,
schema_documents: schema_documents.select { |e| e.record_id == t.id },
dynamic_documents:,
preview_image_attachments:,
expires_at:)
end,
@ -88,6 +76,41 @@ module Api
private
def preload_relations(templates)
schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
dynamic_document_uuids =
templates.flat_map { |t| t.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid') }
dynamic_documents =
if dynamic_document_uuids.present?
DynamicDocument.where(template: templates.map(&:id))
.where(uuid: dynamic_document_uuids)
.preload(current_version: { document_attachment: :blob })
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
else
DynamicDocument.none
end
preview_attachment_ids =
schema_documents.map(&:id) + dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
preview_image_attachments =
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] })
.where(record_id: preview_attachment_ids,
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
[schema_documents, dynamic_documents, preview_image_attachments]
end
def filter_templates(templates, params)
templates = Templates.search(current_user, templates, params[:q])
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active

@ -101,11 +101,18 @@ class StartFormController < ApplicationController
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
Submitter.find_by(slug: params[:resubmit])
submitter = Submitter.find_by(slug: params[:resubmit])
submitter if submitter && can_resubmit?(submitter)
end
end
def can_resubmit?(submitter)
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
end
def authorize_start!
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && @template.archived_at?
return redirect_to start_form_path(@template.slug) if @template.archived_at?
return if @resubmit_submitter

@ -1,10 +1,6 @@
# frozen_string_literal: true
class TemplatesController < ApplicationController
TEMPLATE_FIELDS = %i[id author_id folder_id external_id name slug
schema fields submitters variables_schema preferences
shared_link source archived_at created_at updated_at].freeze
load_and_authorize_resource :template
def show
@ -31,19 +27,7 @@ class TemplatesController < ApplicationController
def new; end
def edit
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [{ schema_documents: [:blob, { preview_images_attachments: :blob }] }]
).call
@template_data =
@template.as_json(only: TEMPLATE_FIELDS).merge(
documents: @template.schema_documents.as_json(
only: %i[id uuid],
methods: %i[metadata signed_key],
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
)
).to_json
@template_data = Templates.serialize_for_builder(@template)
render :edit, layout: 'plain'
end

@ -16,7 +16,12 @@ class TemplatesDetectFieldsController < ApplicationController
page_number = params[:page].presence&.to_i
documents.each do |document|
io = StringIO.new(document.download)
io =
if document.image?
StringIO.new(document.preview_images.joins(:blob).find_by(blob: { filename: ['0.png', '0.jpg'] }).download)
else
StringIO.new(document.download)
end
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
sse.write({ attachment_uuid:, page:, fields: })

@ -4,18 +4,7 @@ class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template
def show
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: %i[metadata signed_key],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
@template_data = Templates.serialize_for_builder(@template)
render :show, layout: 'plain'
end

@ -5,7 +5,9 @@ class TemplatesUploadsController < ApplicationController
layout 'plain'
def show; end
def show
redirect_to root_path if params[:url].blank?
end
def create
url_params = create_file_params_from_url if params[:url].present?

@ -54,6 +54,7 @@ import GoogleDriveFilePicker from './elements/google_drive_file_picker'
import OpenModal from './elements/open_modal'
import BarChart from './elements/bar_chart'
import FieldCondition from './elements/field_condition'
import ConfirmUpload from './elements/confirm_upload'
import * as TurboInstantClick from './lib/turbo_instant_click'
@ -146,6 +147,7 @@ safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
safeRegisterElement('open-modal', OpenModal)
safeRegisterElement('bar-chart', BarChart)
safeRegisterElement('field-condition', FieldCondition)
safeRegisterElement('confirm-upload', ConfirmUpload)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
@ -197,10 +199,35 @@ safeRegisterElement('template-builder', class extends HTMLElement {
}
onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
e.detail.fetchResponse.response.json().then((data) => {
this.component.template.submitters = data.submitters
})
if (e.detail.success) {
if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
e.detail.fetchResponse.response.json().then((data) => {
this.component.template.submitters = data.submitters
})
}
if (e.detail?.formSubmission?.formElement?.action?.endsWith('/prefillable_fields')) {
e.detail.fetchResponse.response.text().then((data) => {
const doc = new DOMParser().parseFromString(data, 'text/html')
const fragment = doc.querySelector('turbo-stream template').content
const prefillableUuidsIndex = {}
fragment.querySelectorAll('[name="field_uuid"]').forEach((field) => {
prefillableUuidsIndex[field.value] = true
})
this.component.template.fields.forEach((field) => {
if (prefillableUuidsIndex[field.uuid]) {
field.prefillable = true
field.readonly = true
} else if (field.prefillable) {
delete field.prefillable
delete field.readonly
}
})
})
}
}
}

@ -0,0 +1,27 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
export default targetable(class extends HTMLElement {
static [target.static] = [
'prompt',
'processing',
'logo'
]
connectedCallback () {
this.form.addEventListener('submit', this.onSubmit)
}
disconnectedCallback () {
this.form.removeEventListener('submit', this.onSubmit)
}
onSubmit = () => {
this.prompt.classList.add('hidden')
this.processing.classList.remove('hidden')
this.logo.classList.add('animate-bounce')
}
get form () {
return this.querySelector('form')
}
})

@ -27,6 +27,7 @@
</label>
<button
v-if="withToday"
type="button"
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]"
>

@ -95,7 +95,7 @@
v-else
id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form"
form="complete_form"
type="submit"
name="completed"
value="true"
@ -120,7 +120,7 @@
>
<button
class="complete-button btn btn-sm btn-neutral text-white px-4"
form="steps_form"
form="complete_form"
type="submit"
name="completed"
value="true"
@ -138,6 +138,14 @@
</span>
</button>
</Teleport>
<form
v-if="!isCompleted && !isInvite"
id="complete_form"
class="hidden"
:action="submitPath"
method="post"
@submit.prevent="submitStep"
/>
<button
v-if="!isFormVisible"
id="expand_form_button"

@ -15,6 +15,7 @@
</template>
</label>
<button
type="button"
class="btn btn-outline btn-sm reupload-button"
@click.prevent="remove"
>

@ -42,6 +42,7 @@
{{ error }}
</div>
<button
type="button"
class="base-button w-full flex justify-center submit-form-button"
@click="restartKba"
>

@ -33,6 +33,7 @@
<div v-else>
<button
v-if="sessionId"
type="button"
disabled
class="base-button w-full modal-save-button"
>
@ -47,6 +48,7 @@
<button
v-else
:id="field.uuid"
type="button"
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
:class="{ disabled: isCreatingCheckout }"
:disabled="isCreatingCheckout"

@ -28,6 +28,12 @@ class DynamicDocument < ApplicationRecord
has_many :versions, class_name: 'DynamicDocumentVersion', dependent: :destroy
has_one :current_version, class_name: 'DynamicDocumentVersion',
primary_key: %i[id sha1],
foreign_key: %i[dynamic_document_id sha1],
dependent: :destroy,
inverse_of: :dynamic_document
attribute :fields, :json
before_validation :set_sha1

@ -75,7 +75,7 @@ class Template < ApplicationRecord
has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions
has_many :schema_dynamic_documents, lambda { |e|
where(uuid: e.schema.select { |e| e['dynamic'] }.pluck('attachment_uuid'))
where(uuid: e.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid'))
}, class_name: 'DynamicDocument', dependent: :destroy, inverse_of: :template
scope :active, -> { where(archived_at: nil) }

@ -36,7 +36,7 @@
<% if signature %>
<div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>">
<img src="<%= signature.url %>" style="height: 130px; width: 100%; object-fit: contain;">
</div>
<% end %>
<a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full">
@ -49,7 +49,7 @@
<% if initials %>
<div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_initials_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= initials.url %>" style="max-height: 200px; width: auto" width="<%= initials.metadata['width'] %>" height="<%= initials.metadata['height'] %>">
<img src="<%= initials.url %>" style="height: 130px; width: 100%; object-fit: contain;">
</div>
<% end %>
<a href="<%= edit_user_initials_path %>" data-turbo-frame="modal" class="base-button w-full">

@ -1,6 +1,6 @@
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %>
<span>16k</span>
<span>17k</span>
</span>
</a>

@ -266,10 +266,12 @@
<div dir="auto">
<% if field['type'].in?(%w[signature initials]) %>
<div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
<% img_height = attachments_index[value].metadata['height'] %>
<img class="object-contain mx-auto" style="<%= 'max-' if img_height %>height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= img_height %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
</div>
<% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
<% img_height = attachments_index[value].metadata['height'] %>
<img class="object-contain mx-auto" style="<%= 'max-' if img_height %>height: 200px" height="<%= img_height %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
<div class="flex flex-col justify-center">
<% Array.wrap(value).each do |val| %>

@ -60,7 +60,7 @@
</div>
<div class="flex flex-wrap gap-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1">
<% if !template.archived_at? && can?(:destroy, template) %>
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving'), title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete, data: { turbo_confirm: t('are_you_sure_') } %>
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving')[..-4], title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete %>
<% end %>
<% if can?(:create, current_account.templates.new(author: current_user)) %>
<%= link_to new_template_clone_path(template), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %>
@ -89,7 +89,7 @@
<% end %>
<% if template.archived_at? %>
<% if can?(:create, template) %>
<%= button_to button_title(title: t('restore'), disabled_with: t('restoring'), icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<%= button_to button_title(title: t('restore'), disabled_with: t('restoring')[..-4], icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<% end %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2">

@ -6,4 +6,4 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %>
<% end %>
<template-builder class="grid" data-template="<%= @template_data %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
<template-builder class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>

@ -1 +1 @@
<template-builder class="grid" data-editable="false" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-template="<%= @template_data %>"></template-builder>
<template-builder class="grid" data-editable="false" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-template="<%= @template_data.to_json %>"></template-builder>

@ -1,18 +1,30 @@
<div class="h-screen">
<div
class="text-center p-8 h-full flex items-center justify-center">
<div>
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto animate-bounce' %>
<span>
<%= t('processing') %>...
</span>
<confirm-upload>
<div class="h-screen">
<div class="text-center p-8 h-full flex items-center justify-center">
<div class="space-y-4">
<div data-target="confirm-upload.logo" class="mx-auto">
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto' %>
</div>
<div data-target="confirm-upload.processing" class="hidden">
<span><%= t('processing') %>...</span>
</div>
<div data-target="confirm-upload.prompt">
<p class="text-lg">
<%= t('open_file_from') %>
<a href="<%= params[:url] %>" target="_blank" class="link" rel="noopener noreferrer nofollow">
<%= params[:filename].presence || params[:url] %>
</a>
</p>
<div class="flex items-center justify-center gap-2 mt-4">
<%= link_to t('cancel'), root_path, class: 'white-button w-44' %>
<%= form_for '', url: templates_upload_path, method: :post do |f| %>
<input type="hidden" name="url" value="<%= params[:url] %>">
<input type="hidden" name="filename" value="<%= params[:filename] %>">
<%= f.button button_title(title: t('open')), class: 'base-button w-44' %>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
<submit-form>
<%= form_for '', url: templates_upload_path, method: :post, class: 'hidden' do %>
<button type="submit"></button>
<input name="url" value="<%= params[:url] %>">
<input name="filename" value="<%= params[:filename] %>">
<% end %>
</submit-form>
</confirm-upload>

@ -25,6 +25,10 @@ module DocuSeal
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
config.active_storage.analyzers = []
config.active_storage.variant_processor = :disabled
config.active_storage.content_types_to_serve_as_binary += %w[
application/javascript
text/javascript

@ -937,6 +937,9 @@ en: &en
your_email_address_has_been_changed: Your email address has been changed
the_email_address_for_your_account_has_been_changed_to_new_email: The email address for your account has been changed to %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: If you did not make this change, please contact us by replying to this email.
open_file_from: 'Open file from'
cancel: Cancel
open: Open
devise:
confirmations:
confirmed: Your email address has been successfully confirmed.
@ -1987,6 +1990,9 @@ es: &es
your_email_address_has_been_changed: Tu dirección de correo electrónico ha sido cambiada
the_email_address_for_your_account_has_been_changed_to_new_email: La dirección de correo electrónico de tu cuenta ha sido cambiada a %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si no realizaste este cambio, contáctanos respondiendo a este correo electrónico.
open_file_from: 'Abrir archivo desde'
cancel: Cancelar
open: Abrir
devise:
confirmations:
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
@ -3037,6 +3043,9 @@ it: &it
your_email_address_has_been_changed: Il tuo indirizzo email è stato modificato
the_email_address_for_your_account_has_been_changed_to_new_email: L'indirizzo email del tuo account è stato modificato in %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se non hai effettuato questa modifica, contattaci rispondendo a questa email.
open_file_from: 'Aprire file da'
cancel: Annulla
open: Apri
devise:
confirmations:
confirmed: Il tuo indirizzo email è stato confermato con successo.
@ -4084,6 +4093,9 @@ fr: &fr
your_email_address_has_been_changed: Votre adresse e-mail a été modifiée
the_email_address_for_your_account_has_been_changed_to_new_email: "L'adresse e-mail de votre compte a été modifiée en %{new_email}."
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si vous n'avez pas effectué ce changement, veuillez nous contacter en répondant à cet e-mail.
open_file_from: 'Ouvrir le fichier depuis'
cancel: Annuler
open: Ouvrir
devise:
confirmations:
confirmed: Votre adresse e-mail a été confirmée avec succès.
@ -5134,6 +5146,9 @@ pt: &pt
your_email_address_has_been_changed: Seu endereço de e-mail foi alterado
the_email_address_for_your_account_has_been_changed_to_new_email: O endereço de e-mail da sua conta foi alterado para %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se você não fez essa alteração, entre em contato conosco respondendo a este e-mail.
open_file_from: 'Abrir arquivo de'
cancel: Cancelar
open: Abrir
devise:
confirmations:
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
@ -6184,6 +6199,9 @@ de: &de
your_email_address_has_been_changed: Ihre E-Mail-Adresse wurde geändert
the_email_address_for_your_account_has_been_changed_to_new_email: Die E-Mail-Adresse Ihres Kontos wurde in %{new_email} geändert.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Wenn Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns bitte, indem Sie auf diese E-Mail antworten.
open_file_from: 'Datei öffnen von'
cancel: Abbrechen
open: Öffnen
devise:
confirmations:
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
@ -7635,6 +7653,9 @@ nl: &nl
your_email_address_has_been_changed: Uw e-mailadres is gewijzigd
the_email_address_for_your_account_has_been_changed_to_new_email: Het e-mailadres van uw account is gewijzigd naar %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Als u deze wijziging niet hebt aangebracht, neem dan contact met ons op door op deze e-mail te antwoorden.
open_file_from: 'Bestand openen van'
cancel: Annuleren
open: Openen
devise:
confirmations:
confirmed: Uw e-mailadres is succesvol bevestigd.

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Accounts
LINK_EXPIRES_AT = 40.minutes
LINK_EXPIRES_AT = ENV.fetch('FILE_URLS_EXPIRE_MINUTES', '40').to_i.minutes
module_function

@ -39,7 +39,7 @@ module LoadBmp
bands = header_data[:bpp] / 8
end
image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
image = Vips::Image.new_from_memory_copy(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
image = image.flip(:vertical) if header_data[:orientation] == -1

@ -43,7 +43,9 @@ module LoadIco
raise ArgumentError, 'Unable to load' unless image_data_bytes && image_data_bytes.bytesize == best_entry[:size]
return Vips::Image.new_from_buffer(image_data_bytes, '') if image_data_bytes.start_with?(PNG_SIGNATURE)
if image_data_bytes.start_with?(PNG_SIGNATURE)
return ImageUtils.load_vips(image_data_bytes, content_type: 'image/png')
end
image = load_image_entry(image_data_bytes, best_entry[:width], best_entry[:height])
@ -200,13 +202,13 @@ module LoadIco
return nil unless pixel_data_string.bytesize == expected_bytes && expected_bytes.positive?
Vips::Image.new_from_memory(
Vips::Image.new_from_memory_copy(
pixel_data_string,
dib_width,
image_pixel_height,
4,
:uchar
)
).copy(interpretation: :srgb)
end
# rubocop:enable Metrics
end

@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'puma/plugin'
require 'redis_client'
# rubocop:disable Metrics
Puma::Plugin.create do
@ -68,7 +69,7 @@ Puma::Plugin.create do
break
rescue RedisClient::CannotConnectError
raise('Unable to connect to redis') if attempt > 10
raise('Unable to connect to redis') if attempt > 30
end
end
end

@ -162,7 +162,7 @@ module Submissions
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
return email.downcase if email.include?(',') ||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) ||
email.match?(/\.(?:gob(?:\.\w+)?|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) ||
email.exclude?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<'))
@ -175,7 +175,9 @@ module Submissions
return email.downcase if domain == fixed_domain
return email.downcase if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
if DidYouMean::Levenshtein.distance(domain, fixed_domain) > 3
threshold = fixed_domain.start_with?('hotmail.') ? 2 : 3
if DidYouMean::Levenshtein.distance(domain, fixed_domain) > threshold
Rails.logger.info("Skipped email fix #{domain}")
return email.downcase

@ -129,6 +129,8 @@ module Submissions
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
file_links_expire_at = Accounts.link_expires_at(submission.account) if with_file_links
timezone = account.timezone
timezone = last_submitter.timezone || account.timezone if with_submitter_timezone
@ -408,7 +410,7 @@ module Submissions
link =
if with_file_links
ActiveStorage::Blob.proxy_url(attachment.blob)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: file_links_expire_at)
else
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
end

@ -29,6 +29,8 @@ module Submissions
with_signature_id_reason =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
file_links_expire_at = Accounts.link_expires_at(submission.account) if with_file_links
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten,
incremental: is_rotate_incremental)
@ -42,7 +44,8 @@ module Submissions
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
with_signature_id:, is_flatten:, with_headings: index.zero?,
with_submitter_timezone:, with_file_links:,
with_signature_id_reason:, with_timestamp_seconds:)
file_links_expire_at:, with_signature_id_reason:,
with_timestamp_seconds:)
end
template = submission.template

@ -151,6 +151,8 @@ module Submissions
with_signature_id_reason =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
file_links_expire_at = Accounts.link_expires_at(submitter.account) if with_file_links
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten,
incremental: is_rotate_incremental)
@ -198,12 +200,14 @@ module Submissions
with_submitter_timezone:,
with_file_links:,
with_timestamp_seconds:,
with_signature_id_reason:)
with_signature_id_reason:,
file_links_expire_at:)
end
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
with_submitter_timezone: false, with_signature_id_reason: true,
with_timestamp_seconds: false, with_file_links: nil)
with_timestamp_seconds: false, with_file_links: nil,
file_links_expire_at: Accounts.link_expires_at(account))
cell_layouters = Hash.new do |hash, valign|
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
end
@ -516,7 +520,7 @@ module Submissions
url =
if with_file_links
ActiveStorage::Blob.proxy_url(attachment.blob)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: file_links_expire_at)
else
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
end
@ -743,7 +747,7 @@ module Submissions
pdf.trailer.info[:Creator] = info_creator
if Docuseal.pdf_format == 'pdf/a-3b'
pdf.task(:pdfa, level: '3b')
pdfa_listener = pdf.task(:pdfa, level: '3b')
pdf.config['font.map'] = PDFA_FONT_MAP
end
@ -759,12 +763,14 @@ module Submissions
begin
pdf.sign(io, write_options: { validate: false }, **sign_params)
rescue HexaPDF::Error, NoMethodError => e
rescue HexaPDF::Error, NoMethodError, TypeError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.instance_variable_get(:@listeners)[:complete_objects].delete(pdfa_listener) if pdfa_listener
begin
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
rescue HexaPDF::Error
rescue HexaPDF::Error, TypeError
pdf.validate(auto_correct: true)
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
end

@ -122,7 +122,7 @@ module Submitters
end
end
def create_attachment!(submitter, file)
def create_attachment!(submitter, file, metadata: {})
raise ParamsError, 'file param is missing' if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase
@ -133,7 +133,8 @@ module Submitters
blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open,
filename: file.original_filename,
content_type: file.content_type)
content_type: file.content_type,
metadata:)
ActiveStorage::Attachment.create!(blob:, name: 'attachments', record: submitter)
end

@ -6,12 +6,6 @@ module Submitters
HEIGHT = 200
LRM = "\u200E"
TRANSPARENT_PIXEL = "\x89PNG\r\n\u001A\n\u0000\u0000\u0000\rIHDR\u0000" \
"\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0004" \
"\u0000\u0000\u0000\xB5\u001C\f\u0002\u0000\u0000\u0000" \
"\vIDATx\xDAc\xFC_\u000F\u0000\u0002\x83\u0001\x804\xC3ڨ" \
"\u0000\u0000\u0000\u0000IEND\xAEB`\x82"
module_function
def call(submitter, with_logo: true)
@ -42,14 +36,15 @@ module Submitters
if with_logo
ImageUtils.load_vips(load_logo(submitter).read)
else
Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
Vips::Image.black(WIDTH, WIDTH, bands: 4).copy(interpretation: :srgb)
end
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
logo = logo.copy(interpretation: :srgb) if logo.interpretation == :multiband
base_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255]).copy(interpretation: :srgb)
base_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255, 255]).copy(interpretation: :srgb)
opacity_layer = Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
opacity_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255, 127]).copy(interpretation: :srgb)
text = build_text_image(submitter)

@ -27,7 +27,9 @@ module Submitters
text_mask = Vips::Image.black(text_image.width, text_image.height)
text_mask.bandjoin(text_image).copy(interpretation: :b_w).write_to_buffer('.png')
image = text_mask.bandjoin(text_image).copy(interpretation: :b_w)
[image.write_to_buffer('.png'), image.width, image.height]
end
end
end

@ -260,7 +260,7 @@ module Submitters
end
def find_or_create_blob_from_text(account, text, type)
data = Submitters::GenerateFontImage.call(text, font: type)
data, width, height = Submitters::GenerateFontImage.call(text, font: type)
checksum = Digest::MD5.base64digest(data)
@ -268,7 +268,9 @@ module Submitters
blob || ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(data),
filename: "#{type}.png"
filename: "#{type}.png",
content_type: 'image/png',
metadata: { analyzed: true, identified: true, width:, height: }
)
end

@ -3,6 +3,10 @@
module Templates
COLOR_REGEXP = /\A(#(?:[0-9a-f]{3}|[0-9a-f]{6})|[a-z]+)\z/i
TEMPLATE_BUILDER_FIELDS = %i[id author_id folder_id external_id name slug
schema fields submitters variables_schema preferences
shared_link source archived_at created_at updated_at].freeze
EXPIRATION_DURATIONS = {
one_day: 1.day,
two_days: 2.days,
@ -91,4 +95,16 @@ module Templates
Time.current + EXPIRATION_DURATIONS[default_expire_at_duration]
end
end
def serialize_for_builder(template)
data = template.as_json(only: TEMPLATE_BUILDER_FIELDS)
data['documents'] = template.schema_documents.preload(:blob, { preview_images_attachments: :blob }).as_json(
only: %i[id uuid],
methods: %i[metadata signed_key],
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
)
data
end
end

@ -77,7 +77,7 @@ module Templates
split_page: false, aspect_ratio: false, padding: nil, page_number: nil)
return [[], nil] if page_number && page_number != 0
image = ImageUtils.load_vips(io.read, content_type: attachment.content_type)
image = ImageUtils.load_vips(io.read)
fields = inference.call(image, confidence:, nms:, nmm:, split_page:,
temperature:, aspect_ratio:, padding:)
@ -193,7 +193,7 @@ module Templates
data, width, height = page.render_to_bitmap(size_key => size)
Vips::Image.new_from_memory(data, width, height, 4, :uchar)
Vips::Image.new_from_memory_copy(data, width, height, 4, :uchar)
end
def sort_fields(fields, y_threshold: 0.01)

@ -107,7 +107,7 @@ module Templates
bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
image = Vips::Image.new_from_memory(bytes, width, height, 4, :uchar)
image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar)
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(image, page_number) }
ensure
@ -201,7 +201,7 @@ module Templates
doc_page.close
image = Vips::Image.new_from_memory(bytes, width, height, 4, :uchar)
image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar)
blob = build_and_upload_blob(image, page_number, PREVIEW_FORMAT)

@ -14,27 +14,25 @@ module Templates
module_function
def call(template, schema_documents: template.schema_documents.preload(:blob), preview_image_attachments: nil,
expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
def call(template, schema_documents: template.schema_documents.preload(:blob), dynamic_documents: nil,
preview_image_attachments: nil, expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
json = template.as_json(SERIALIZE_PARAMS)
preview_image_attachments ||=
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.jpg', '0.png'] })
.where(record_id: schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
dynamic_documents ||= preload_dynamic_documents(template)
json['documents'] = template.schema.filter_map do |item|
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
preview_image_attachments ||= preload_preview_image_attachments(schema_documents, dynamic_documents)
unless attachment
Rollbar.error("Documents missing: #{template.id}") if defined?(Rollbar)
json['documents'] = template.schema.filter_map do |item|
if item['dynamic']
dynamic_document = dynamic_documents.find { |e| e.uuid == item['attachment_uuid'] }
next
attachment = dynamic_document.current_version&.document_attachment
end
attachment ||= schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
next unless attachment
first_page_blob = preview_image_attachments.find { |e| e.record_id == attachment.id }&.blob
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
@ -49,5 +47,26 @@ module Templates
json
end
def preload_dynamic_documents(template)
return DynamicDocument.none if template.schema.none? { |item| item['dynamic'] }
template.schema_dynamic_documents
.preload(current_version: { document_attachment: :blob })
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
end
def preload_preview_image_attachments(schema_documents, dynamic_documents)
record_ids =
schema_documents.map(&:id) +
dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.jpg', '0.png'] })
.where(record_id: record_ids,
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
end
end
end

@ -42,9 +42,7 @@ RSpec.describe 'Template' do
it 'archives a template' do
expect do
accept_confirm('Are you sure?') do
click_button 'Archive'
end
click_button 'Archive'
end.to change { Template.active.count }.by(-1)
expect(page).to have_content('Template has been archived')

@ -0,0 +1,40 @@
# frozen_string_literal: true
RSpec.describe 'Templates Upload' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
before do
sign_in(user)
end
context 'when url param is present' do
let(:file_url) { 'https://example.com/document.pdf' }
before do
stub_request(:get, file_url).to_return(
body: Rails.root.join('spec/fixtures/sample-document.pdf').read,
headers: { 'Content-Type' => 'application/pdf' }
)
end
it 'shows a confirm page and creates template on submit' do
visit "/new?url=#{CGI.escape(file_url)}"
expect(page).to have_text('Open file from')
expect(page).to have_link('example.com/document.pdf')
click_button 'Open'
expect(Template.last.name).to eq('document')
end
end
context 'when url param is missing' do
it 'redirects to root' do
visit '/new'
expect(page).to have_current_path(root_path)
end
end
end
Loading…
Cancel
Save