Compare commits

...
64 Commits
Author SHA1 Message Date
Alex Turchyn 27a0d10525 Merge from docusealco/wip 2026-08-25 13:46:49 +03:00
Pete Matsyburka d25f0812ca reset decline values 2026-08-24 16:38:05 +03:00
Pete Matsyburka 9e2aa6f8c8 pass fetch options 2026-08-24 14:43:13 +03:00
Pete Matsyburka 11c76bf554 refactor 2026-08-24 09:47:34 +03:00
Pete Matsyburka e5733d88c0 remove uuids scope 2026-08-23 22:15:02 +03:00
Pete Matsyburka 6d38c9f375 adjust resubmit 2026-08-23 21:23:27 +03:00
Pete Matsyburka 91ec29ad2c add i18n 2026-08-23 19:23:05 +03:00
Pete Matsyburka d374c7fa10 fix log 2026-08-23 19:04:18 +03:00
Pete Matsyburka ca0daf6d0b normalize 2fa email 2026-08-23 18:52:12 +03:00
Pete Matsyburka 076c3d805a fix find_or_initialize_submitter 2026-08-23 18:49:32 +03:00
Pete Matsyburka 1f149ac67a new form if fields changed 2026-08-23 18:01:00 +03:00
Pete Matsyburka 616f7b4fd8 adjsut resubmit 2026-08-23 17:48:04 +03:00
Pete Matsyburka 6f4527038b fix redirect 2026-08-23 17:38:17 +03:00
Pete Matsyburka 8ed2883504 adjust email 2fa form 2026-08-23 17:34:17 +03:00
Pete Matsyburka 651b84a460 fix redirect status 2026-08-23 17:24:44 +03:00
Pete Matsyburka 73c4f158ff add self source 2026-08-23 17:19:50 +03:00
Pete Matsyburka 6f1cc34433 adjust log 2026-08-23 16:51:21 +03:00
Pete Matsyburka a77029ffcb start form remember session 2026-08-23 16:51:15 +03:00
Pete Matsyburka 0480572ef5 refactor start form 2026-08-23 15:45:43 +03:00
Pete Matsyburka 807463c465 optimize shared link 2026-08-23 15:45:43 +03:00
Pete Matsyburka 0f61053976 remove secrets yml 2026-08-23 11:50:12 +03:00
Pete Matsyburka b8cf525034 fix cycle safe 2026-08-21 18:56:22 +03:00
Pete Matsyburka fef203daed adjust notification settings 2026-08-21 17:45:19 +03:00
Pete Matsyburka 41b38cc594 fix js error on redirect 2026-08-21 12:00:29 +03:00
Pete Matsyburka 4e4354af99 fix RTL reverse 2026-08-20 17:26:54 +03:00
Pete Matsyburka 113b61588b process pdf from doc 2026-08-20 13:21:24 +03:00
Pete Matsyburka c1aac6b763 add stats 2026-08-19 12:58:24 +03:00
Alex Turchyn f85fd3cff7 add personalization tooltips 2026-08-18 14:24:55 +03:00
Alex Turchyn 004a22c1c8 Merge from docusealco/wip 2026-08-17 12:31:40 +03:00
Pete Matsyburka 69343b1bdd fix send completed emails 2026-08-17 09:16:20 +03:00
Pete Matsyburka 81a306928c fix ci 2026-08-16 10:17:48 +03:00
Alex Turchyn 2bf3d42ed6 add translations 2026-08-16 09:51:41 +03:00
Pete Matsyburka eec08266c9 fix page preview open 2026-08-16 09:45:04 +03:00
Pete Matsyburka cbbc75b485 fix CI 2026-08-15 22:09:08 +03:00
Pete Matsyburka 3d93bbe5aa improve flatten 2026-08-15 21:08:51 +03:00
Pete Matsyburka 5308e3e15b process attachment v2 2026-08-15 21:08:35 +03:00
Pete Matsyburka 22c146d564 adjust pdfium 2026-08-15 19:08:54 +03:00
Pete Matsyburka c8d3af9841 import acro form 2026-08-15 19:08:54 +03:00
Pete Matsyburka ee1d45d461 update dep 2026-08-15 19:08:54 +03:00
Pete Matsyburka 27d075ed63 optimize merge pdf 2026-08-15 19:08:54 +03:00
Pete Matsyburka df63acf0c8 optimize decrypt 2026-08-15 19:08:52 +03:00
Alex Turchyn 185827c951 add Pdfium acro fields detector 2026-08-14 15:29:41 +03:00
Pete Matsyburka e361cbabf4 adjust verify signature 2026-08-14 15:29:41 +03:00
Pete Matsyburka e867bf5c48 use json serializer 2026-08-14 12:16:25 +03:00
Pete Matsyburka 726d0ea4f3 validate url 2026-08-13 15:46:43 +03:00
Pete Matsyburka 90be34b243 update gem 2026-08-11 22:05:14 +03:00
Pete Matsyburka e87c06d811 uniq bcc addresses 2026-08-11 17:23:28 +03:00
Alex Turchyn 54454d1dfc Merge from docusealco/wip 2026-08-11 17:16:45 +03:00
Pete Matsyburka 726174deff authorize reminder config 2026-08-11 08:55:17 +03:00
Pete Matsyburka 52eb8ac863 add log 2026-08-10 15:21:22 +03:00
Pete Matsyburka 97d91d8f27 fix view only download 2026-08-10 12:06:42 +03:00
Pete Matsyburka dd358a8401 fix infinite loop 2026-08-10 11:48:05 +03:00
Pete Matsyburka 8e254b7d3c fix infinite loop 2026-08-10 06:01:20 +03:00
Pete Matsyburka b9018fa442 add completed download scope 2026-08-09 20:14:31 +03:00
Pete Matsyburka 09310f3690 guard save template message 2026-08-09 19:53:19 +03:00
Pete Matsyburka 2dd38260ba guard email 2fa send 2026-08-09 19:51:07 +03:00
Pete Matsyburka ffb3247cdc add authorize user 2026-08-09 18:37:53 +03:00
Pete Matsyburka c59c470321 sanitize search query 2026-08-09 16:01:12 +03:00
Pete Matsyburka f35510ffd6 adjust with reason 2026-08-08 09:00:35 +03:00
Pete Matsyburka eeec852b3b adjust preview cache 2026-08-07 21:54:55 +03:00
Pete Matsyburka 485b7f2c8f fix dynamic variable parse 2026-08-07 14:19:28 +03:00
Alex Turchyn b6498ec049 add PayPal translations 2026-08-07 10:12:52 +03:00
Pete Matsyburka dc1de72e18 fix mysql migrations 2026-08-06 17:50:03 +03:00
Pete Matsyburka 7dddef4c94 fix date time formats 2026-08-04 11:07:13 +03:00
68 changed files with 2632 additions and 441 deletions

No files matched your search

+19 -25
View File
@@ -111,48 +111,39 @@ jobs:
rspec: rspec:
name: RSpec name: RSpec
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 15
container:
image: ruby:4.0.5-alpine
services: services:
postgres: postgres:
image: postgres:14 image: postgres:18
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: docuseal_test POSTGRES_DB: docuseal_test
ports: ["5432:5432"]
options: >- options: >-
--health-cmd pg_isready --health-cmd "pg_isready -U postgres -d docuseal_test"
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
steps: steps:
- name: Install packages
run: apk add --no-cache build-base git libpq-dev yaml-dev nodejs yarn tar zstd gzip wget unzip vips leptonica chromium
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: 20.19.0
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
with:
chrome-version: 125
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: node_modules path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-musl-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-musl-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-gem- ${{ runner.os }}-musl-gem-
- name: Install dependencies - name: Install dependencies
env: env:
RAILS_ENV: test RAILS_ENV: test
@@ -161,17 +152,20 @@ jobs:
bundle config path vendor/bundle bundle config path vendor/bundle
bundle install --jobs 4 --retry 4 bundle install --jobs 4 --retry 4
yarn install yarn install
sudo apt-get update wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip"
sudo apt-get install -y libvips liblept5 case "$(uname -m)" in
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;;
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;;
rm -f pdfium-linux.tgz esac | sha256sum -c -
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
rm -rf pdfium-linux.zip /tmp/pdfium-linux
- name: Run - name: Run
env: env:
RAILS_ENV: test RAILS_ENV: test
NODE_ENV: test NODE_ENV: test
COVERAGE: true COVERAGE: true
DATABASE_URL: postgres://postgres:postgres@localhost:5432/docuseal_test DATABASE_URL: postgres://postgres:postgres@postgres:5432/docuseal_test
run: | run: |
bundle exec rake db:create bundle exec rake db:create
bundle exec rake db:migrate bundle exec rake db:migrate
+3 -3
View File
@@ -9,10 +9,10 @@ RUN apk --no-cache add wget unzip && \
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \ wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \ wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \ wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260613/pdfium-musl-$(uname -m).zip" && \ wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip" && \
case "$(uname -m)" in \ case "$(uname -m)" in \
x86_64) echo "2c953ff72ee2dda07e7fc577e25841cc3d6464468a7c5adfaea574efcbc3b90b pdfium-linux.zip" ;; \ x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;; \
aarch64) echo "23bbe287d2753fdb05741c7660647eb0ef0d2e4da2ce0722bfa9d9d455bd64e2 pdfium-linux.zip" ;; \ aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;; \
esac | sha256sum -c - && \ esac | sha256sum -c - && \
mkdir -p /pdfium-linux && \ mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /pdfium-linux unzip -q pdfium-linux.zip -d /pdfium-linux
+1 -1
View File
@@ -271,7 +271,7 @@ GEM
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.21.1) json (2.21.2)
jwt (3.2.0) jwt (3.2.0)
base64 base64
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
+1 -1
View File
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
rescue_from RateLimit::LimitApproached do |e| rescue_from RateLimit::LimitApproached do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests redirect_back_or_to root_path, alert: 'Too many requests'
end end
if Rails.env.production? || Rails.env.test? if Rails.env.production? || Rails.env.test?
@@ -8,6 +8,7 @@ class NotificationsSettingsController < ApplicationController
before_action :build_account_config, only: :create before_action :build_account_config, only: :create
authorize_resource :account_config, only: :create authorize_resource :account_config, only: :create
before_action :authorize_email_reminders!, only: :create
def index; end def index; end
@@ -21,6 +22,14 @@ class NotificationsSettingsController < ApplicationController
private private
def authorize_email_reminders!
return unless Docuseal.multitenant?
return if @account_config.key != AccountConfig::SUBMITTER_REMINDERS
return if can?(:manage, :email_reminders)
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unlock_with_docuseal_pro')
end
def build_account_config def build_account_config
@account_config = @account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key]) AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key])
@@ -5,6 +5,10 @@ class PreviewDocumentPageController < ActionController::API
FORMAT = Templates::ProcessDocument::FORMAT FORMAT = Templates::ProcessDocument::FORMAT
TMPFILE_PREFIX = 'attachment-'
TMPFILE_TTL = 5.minutes
TMPFILE_MAX_TOTAL_SIZE = Docuseal.multitenant? ? 400.megabytes : 1.gigabyte
def show def show
result_data = result_data =
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment) ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
@@ -30,34 +34,63 @@ class PreviewDocumentPageController < ActionController::API
allow_other_host: true allow_other_host: true
end end
file_path =
if attachment.service.name == :disk
ActiveStorage::Blob.service.path_for(attachment.key)
else
find_or_create_document_tempfile_path(attachment)
end
preview_image = preview_image =
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i) open_attachment_io(attachment) do |io|
Templates::ProcessDocument.generate_pdf_preview_from_io(attachment, io, params[:id].to_i)
end
redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true
end end
def find_or_create_document_tempfile_path(attachment) def open_attachment_io(attachment, &)
file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}" return File.open(ActiveStorage::Blob.service.path_for(attachment.key), 'rb', &) if attachment.service.name == :disk
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f| file_name = "#{TMPFILE_PREFIX}#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
f.flock(File::LOCK_EX)
file_path = File.join(Dir.tmpdir, file_name)
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |file|
file.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate # rubocop:disable Style/ZeroLengthPredicate
if f.size.zero? if file.size.zero?
f.binmode cleanup_stale_tempfiles
f.write(attachment.download) file.binmode
file.write(attachment.download)
else
FileUtils.touch(file_path)
end end
# rubocop:enable Style/ZeroLengthPredicate # rubocop:enable Style/ZeroLengthPredicate
end
file_path file.flock(File::LOCK_UN)
yield file
end
end
def cleanup_stale_tempfiles
entries =
Dir.glob(File.join(Dir.tmpdir, "#{TMPFILE_PREFIX}*")).filter_map do |path|
stat = File.stat(path)
[path, stat.mtime, stat.size]
rescue Errno::ENOENT
nil
end
total_size = entries.sum(&:last)
stale_time = TMPFILE_TTL.ago
entries.sort_by(&:second).each do |path, mtime, size|
break if mtime > stale_time && total_size <= TMPFILE_MAX_TOTAL_SIZE
File.unlink(path)
total_size -= size
rescue Errno::ENOENT
nil
end
end end
end end
+44 -133
View File
@@ -8,13 +8,9 @@ class StartFormController < ApplicationController
around_action :with_browser_locale, only: %i[show update completed] around_action :with_browser_locale, only: %i[show update completed]
before_action :maybe_redirect_com, only: %i[show completed] before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update
before_action :load_template before_action :load_template
before_action :authorize_start!, only: :update before_action :authorize_start!, only: :update
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show def show
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa'] if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
raise ActionController::RoutingError, I18n.t('not_found') raise ActionController::RoutingError, I18n.t('not_found')
@@ -23,8 +19,7 @@ class StartFormController < ApplicationController
if @template.shared_link? if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id) @submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(account_id: @template.account_id, .submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first || uuid: Submitters::StartForm.first_submitter_uuid(@template))
@template.submitters.first)['uuid'])
render :email_verification if params[:email_verification] render :email_verification if params[:email_verification]
else else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -41,24 +36,28 @@ class StartFormController < ApplicationController
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank) redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
else else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message @error_message = multiple_submitters_error_message
return render :show, status: :unprocessable_content return render :show, status: :unprocessable_content
end end
if (is_new_record = @submitter.new_record?) if (is_new_record = @submitter.new_record?)
assign_submission_attributes(@submitter, @template) Submitters::StartForm.assign_submission_attributes(
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission) Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent) @submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end end
if @template.preferences['shared_link_2fa'] == true if require_link_2fa?(@template, @submitter)
handle_require_2fa(@submitter, is_new_record:) handle_require_2fa(@template, @submitter)
elsif @submitter.errors.blank? && @submitter.save elsif @submitter.errors.blank? && @submitter.save
enqueue_new_submitter_jobs(@submitter) if is_new_record Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
@@ -88,41 +87,40 @@ class StartFormController < ApplicationController
private private
def enqueue_new_submitter_jobs(submitter) def find_or_initialize_submitter(template, submitter_params)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created') required_fields = template.preferences.fetch('link_form_fields', ['email'])
SearchEntries.enqueue_reindex(submitter) blank_fields = required_fields.select { |key| submitter_params[key].blank? }
expire_at = submitter.submission.expire_at submitter =
if blank_fields.present?
return unless expire_at Submitter.new(submitter_params)
else
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id, Submitters::StartForm.find_or_initialize_submitter(
'expire_at' => expire_at.to_i) template, submitter_params, exclude_completed: params[:resubmit].present?,
end request:, current_user:
)
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
submitter = Submitter.find_by(slug: params[:resubmit])
submitter if submitter && can_resubmit?(submitter)
end end
blank_fields.each { |key| submitter.errors.add(key.to_sym, :blank) }
submitter
end end
def can_resubmit?(submitter) def require_link_2fa?(template, submitter)
submitter.completed_at? && submitter.completed_at > 14.days.ago && return true if template.preferences['shared_link_2fa'] == true
%w[api embed mcp].exclude?(submitter.submission.source) && return false if cookies.encrypted[:start_form_slug] == submitter.slug
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false return false if current_user && submitter.email == current_user.email &&
current_user.account_id == submitter.account_id
!submitter.new_record?
end end
def authorize_start! def authorize_start!
is_archived = @template.archived_at? || @template.account.archived_at? is_archived = @template.archived_at? || @template.account.archived_at?
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && is_archived
return redirect_to start_form_path(@template.slug) if is_archived return redirect_to start_form_path(@template.slug) if is_archived
return if @resubmit_submitter
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template)) return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -130,90 +128,14 @@ class StartFormController < ApplicationController
redirect_to start_form_path(@template.slug) redirect_to start_form_path(@template.slug)
end end
def find_or_initialize_submitter(template, submitter_params)
required_fields = template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
find_params = required_params.except('name')
submitter = Submitter.new if find_params.compact_blank.blank?
submitter ||=
Submitter
.where(submission: template.submissions.non_expired.active)
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(find_params)
submitter = Submitter.new(find_params) if submitter.submission&.completed_at? && submitter.viewer?
submitter.name = required_params['name'] if submitter.new_record?
unless @resubmit_submitter
required_params.each do |key, value|
submitter.errors.add(key.to_sym, :blank) if value.blank?
end
end
submitter
end
def assign_submission_attributes(submitter, template)
submitter.assign_attributes(
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
ip: request.remote_ip,
ua: request.user_agent,
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: @resubmit_submitter&.metadata.presence || {}
)
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
if submitter.values.present?
@resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end
end
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
expire_at: Templates.build_default_expire_at(template),
submitters: [submitter],
source: :link)
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
submitter.account_id = submitter.submission.account_id
submitter
end
def filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template.submitters)
end
def submitter_params def submitter_params
return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
end end
end end
def load_template def load_template
@template = @template = Template.find_by!(slug: params[:slug] || params[:start_form_slug])
if @resubmit_submitter
@resubmit_submitter.template
else
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
end end
def multiple_submitters_error_message def multiple_submitters_error_message
@@ -224,38 +146,27 @@ class StartFormController < ApplicationController
end end
end end
def handle_require_2fa(submitter, is_new_record:) def handle_require_2fa(template, submitter)
return render :show, status: :unprocessable_content if submitter.errors.present? return render :show, status: :unprocessable_content if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter) if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request, is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
if submitter.save
enqueue_new_submitter_jobs(submitter) if is_new_record
if is_otp_verified
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
cookies.encrypted[:email_2fa_slug] =
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_content
end
else else
if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true
Rollbar.info("2FA link requested: #{submitter.id}")
end
Submitters.send_shared_link_email_verification_code(submitter, request:) Submitters.send_shared_link_email_verification_code(submitter, request:)
render :email_verification render :email_verification
end end
rescue Submitters::StartForm::NotSaved
render :show, status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(submitter.submission.template.slug, redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
params: submitter_params.merge(email_verification: true)),
alert: e.message alert: e.message
rescue RateLimit::LimitApproached rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug, redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts) alert: I18n.t(:too_many_attempts)
end end
end end
@@ -6,12 +6,17 @@ class StartFormEmail2faSendController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
def create before_action :load_template
@template = Template.find_by!(slug: params[:slug]) before_action :authorize_start!
def create
@submitter = @template.submissions.new(account_id: @template.account_id) @submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id) .submitters.new(**submitter_params, account_id: @template.account_id)
if @submitter.email.blank?
return redirect_to start_form_path(@template.slug), alert: I18n.t(:provide_your_email_to_start)
end
Submitters.send_shared_link_email_verification_code(@submitter, request:) Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend] redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
@@ -25,7 +30,34 @@ class StartFormEmail2faSendController < ApplicationController
private private
def load_template
@template = Template.find_by!(slug: params[:slug])
end
def authorize_start!
is_archived = @template.archived_at? || @template.account.archived_at?
return redirect_to start_form_path(@template.slug) if is_archived
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
(@template.preferences['shared_link_2fa'] == true || submitter_exists?(@template, params))
Rollbar.warning("Not 2FA shared template: #{@template.id}") if defined?(Rollbar)
redirect_to start_form_path(@template.slug)
end
def submitter_exists?(template, params)
email = Submissions.normalize_email(params.dig(:submitter, :email))
return false if email.blank?
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true).exists?(email:)
end
def submitter_params def submitter_params
params.require(:submitter).permit(:name, :email, :phone) params.require(:submitter).permit(:name, :email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end end
end end
@@ -0,0 +1,99 @@
# frozen_string_literal: true
class StartFormResubmitController < ApplicationController
layout 'form'
skip_before_action :authenticate_user!
skip_authorization_check
around_action :with_browser_locale
before_action :load_resubmit_submitter
before_action :load_template
before_action :authorize_start!
def update
@submitter = find_or_initialize_submitter(@template, @resubmit_submitter)
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
return render 'start_form/show', status: :unprocessable_content
end
if (is_new_record = @submitter.new_record?)
Submitters::StartForm.assign_submission_attributes(
@submitter, @template,
ip: request.remote_ip, user_agent: request.user_agent, resubmit_submitter: @resubmit_submitter,
source: @resubmit_submitter.submission.source_self? ? :self : :link
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter)
elsif @submitter.save
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
else
render 'start_form/show', status: :unprocessable_content
end
end
private
def find_or_initialize_submitter(template, resubmit_submitter)
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true)
.find_by(slug: cookies.encrypted[:start_form_slug],
email: resubmit_submitter.email) || Submitter.new
end
def load_resubmit_submitter
@resubmit_submitter = Submitter.find_by(slug: params[:resubmit])
raise ActiveRecord::RecordNotFound if @resubmit_submitter.blank? ||
!Submitters::StartForm.can_resubmit?(@resubmit_submitter)
end
def load_template
@template = @resubmit_submitter.template
end
def authorize_start!
redirect_to submit_form_path(@resubmit_submitter.slug) if @template.archived_at? || @template.account.archived_at?
end
def multiple_submitters_error_message
if current_user&.account_id == @template.account_id
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
else
I18n.t('not_found')
end
end
def handle_require_2fa(submitter)
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request,
is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
render 'start_form/email_verification'
end
rescue Submitters::StartForm::NotSaved
render 'start_form/show', status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
flash.now[:alert] = e.message
render 'start_form/email_verification'
rescue RateLimit::LimitApproached
flash.now[:alert] = I18n.t(:too_many_attempts)
render 'start_form/email_verification'
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
class StartFormSelfController < ApplicationController
load_resource :template, parent: false
around_action :with_browser_locale
before_action :authorize_start!
def update
@submitter = Submitters::StartForm.build_submitters_scope(@template, exclude_completed: true, source: :self)
.find_or_initialize_by(email: current_user.email)
@submitter = Submitter.new(email: current_user.email) if @submitter.persisted? &&
outdated_template_fields?(@submitter, @template)
if (is_new_record = @submitter.new_record?)
@submitter.name = current_user.full_name
Submitters::StartForm.assign_submission_attributes(
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent, source: :self
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
@submitter.save!
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
end
private
def outdated_template_fields?(submitter, template)
template_fields = submitter.submission.template_fields
template_fields.present? && template_fields != template.fields
end
def authorize_start!
authorize!(:read, @template)
redirect_to template_path(@template) if @template.archived_at?
end
end
+1 -1
View File
@@ -40,7 +40,7 @@ class SubmissionsController < ApplicationController
def create def create
return redirect_to template_path(@template), alert: I18n.t('template_has_been_archived') if @template.archived_at? return redirect_to template_path(@template), alert: I18n.t('template_has_been_archived') if @template.archived_at?
save_template_message(@template, params) if params[:save_message] == '1' save_template_message(@template, params) if params[:save_message] == '1' && can?(:update, @template)
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1' [params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
@@ -10,18 +10,19 @@ class SubmitFormCompletedDownloadController < ApplicationController
def index def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? @submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid = signature_valid = @submitter&.slug == submitter_slug
if @submitter&.slug == submitter_slug
true
else
@submitter = nil
end
@submitter ||= Submitter.find_by!(slug: submitter_slug) @submitter = Submitter.find_by!(slug: submitter_slug) unless signature_valid
unless completed_submitter?(@submitter)
Rollbar.error("Not completed: #{@submitter.id}") if defined?(Rollbar)
return head :not_found
end
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at? Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last
return head :not_found unless last_submitter return head :not_found unless last_submitter
@@ -60,6 +61,10 @@ class SubmitFormCompletedDownloadController < ApplicationController
end end
end end
def completed_submitter?(submitter)
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
end
def current_user_submitter?(submitter) def current_user_submitter?(submitter)
current_user && current_ability.can?(:read, submitter) current_user && current_ability.can?(:read, submitter)
end end
@@ -19,7 +19,7 @@ class SubmitFormDeclineController < ApplicationController
request) request)
ApplicationRecord.transaction do ApplicationRecord.transaction do
@submitter.update!(declined_at: Time.current) @submitter.update!(declined_at: Time.current, values: @submitter.preferences['default_values'] || {})
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] }) SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
end end
@@ -8,9 +8,6 @@ class SubmitFormEmail2fasController < ApplicationController
before_action :load_submitter before_action :load_submitter
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def create def create
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true) RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
@@ -20,7 +17,8 @@ class SubmitFormEmail2fasController < ApplicationController
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email }) SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
cookies.encrypted[:email_2fa_slug] = cookies.encrypted[:email_2fa_slug] =
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS } { value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now,
**Submitters::StartForm::COOKIES_DEFAULTS }
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
@@ -36,7 +36,7 @@ class SubmittersAutocompleteController < ApplicationController
else else
column = Submitter.arel_table[field.to_sym] column = Submitter.arel_table[field.to_sym]
term = "#{params[:q].downcase}%" term = "#{ActiveRecord::Base.sanitize_sql_like(params[:q].downcase)}%"
submitters.where(column.matches(term)) submitters.where(column.matches(term))
end end
@@ -6,7 +6,9 @@ class TemplatesUploadsController < ApplicationController
layout 'plain' layout 'plain'
def show def show
redirect_to root_path if params[:url].blank? url_uri = Addressable::URI.parse(params[:url].to_s)
redirect_to root_path if url_uri.normalized_scheme != 'https' || url_uri.host.blank?
end end
def create def create
+2
View File
@@ -75,6 +75,8 @@ class UsersController < ApplicationController
authorize!(:manage, account) authorize!(:manage, account)
@user.account = account @user.account = account
authorize!(:create, @user)
end end
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password]))) if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
@@ -4,16 +4,16 @@ class VerifyPdfSignatureController < ApplicationController
skip_authorization_check skip_authorization_check
def create def create
pdfs =
params[:files].map do |file|
HexaPDF::Document.new(io: file.open)
end
trusted_certs = Accounts.load_trusted_certs(current_account) trusted_certs = Accounts.load_trusted_certs(current_account)
signatures =
params[:files].map do |file|
VerifyPdfSignature.call(file.open, trusted_certs)
end
render turbo_stream: turbo_stream.replace('result', partial: 'result', render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: }) locals: { signatures:, files: params[:files] })
rescue HexaPDF::MalformedPDFError rescue Pdfium::PdfiumError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result')) render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result'))
end end
end end
+1 -1
View File
@@ -101,7 +101,7 @@
</template> </template>
</div> </div>
<div> <div>
{{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }} {{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
</div> </div>
</div> </div>
</div> </div>
+4 -1
View File
@@ -560,6 +560,7 @@
:fields="formulaFields" :fields="formulaFields"
:values="values" :values="values"
:readonly-values="readonlyFieldValues" :readonly-values="readonlyFieldValues"
:fetch-options="fetchOptions"
:provider="paymentProvider" :provider="paymentProvider"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@@ -573,6 +574,7 @@
:empty-value-required-step="emptyValueRequiredStep" :empty-value-required-step="emptyValueRequiredStep"
:field="currentField" :field="currentField"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:fetch-options="fetchOptions"
:values="values" :values="values"
@submit="!isSubmitting && submitStep()" @submit="!isSubmitting && submitStep()"
/> />
@@ -585,6 +587,7 @@
:empty-value-required-step="emptyValueRequiredStep" :empty-value-required-step="emptyValueRequiredStep"
:field="currentField" :field="currentField"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:fetch-options="fetchOptions"
:values="values" :values="values"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="!isSubmitting && submitStep()"
@@ -1776,7 +1779,7 @@ export default {
window.location.href = sanitizeUrl(this.completedRedirectUrl) window.location.href = sanitizeUrl(this.completedRedirectUrl)
} else { } else {
this.$nextTick(() => { this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode?.getRootNode() || document
const completedEl = root.getElementById('form_completed') const completedEl = root.getElementById('form_completed')
if (completedEl) { if (completedEl) {
+10 -3
View File
@@ -312,6 +312,11 @@ export default {
values: { values: {
type: Object, type: Object,
required: true required: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
} }
}, },
emits: ['submit'], emits: ['submit'],
@@ -494,8 +499,9 @@ export default {
const resp = await fetch(this.baseUrl + '/api/kba', { const resp = await fetch(this.baseUrl + '/api/kba', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
body: JSON.stringify(payload) ...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}) })
const data = await resp.json() const data = await resp.json()
@@ -550,7 +556,8 @@ export default {
reference: this.reference, reference: this.reference,
submitter_slug: this.submitterSlug submitter_slug: this.submitterSlug
}), }),
headers: { 'Content-Type': 'application/json' } ...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}) })
const data = await resp.json() const data = await resp.json()
@@ -121,6 +121,11 @@ export default {
type: String, type: String,
required: false, required: false,
default: '' default: ''
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
} }
}, },
emits: ['focus', 'submit', 'update:model-value', 'attached'], emits: ['focus', 'submit', 'update:model-value', 'attached'],
@@ -234,7 +239,8 @@ export default {
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug submitter_slug: this.submitterSlug
}), }),
headers: { 'Content-Type': 'application/json' } ...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => { }).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) { if (resp.status === 422 || resp.status === 500) {
this.isProcessing = false this.isProcessing = false
@@ -274,7 +280,8 @@ export default {
check_status: checkStatus, check_status: checkStatus,
referer: this.normalizedPaymentUrl() referer: this.normalizedPaymentUrl()
}), }),
headers: { 'Content-Type': 'application/json' } ...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => { }).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) { if (resp.status === 422 || resp.status === 500) {
const data = await resp.json() const data = await resp.json()
@@ -91,6 +91,11 @@ export default {
submitterSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
} }
}, },
emits: ['focus', 'submit', 'update:model-value', 'attached'], emits: ['focus', 'submit', 'update:model-value', 'attached'],
@@ -163,7 +168,8 @@ export default {
submitter_slug: this.submitterSlug, submitter_slug: this.submitterSlug,
redirect_url: document.location.href redirect_url: document.location.href
}), }),
headers: { 'Content-Type': 'application/json' } ...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => { }).then(async (resp) => {
this.eidEasyData = await resp.json() this.eidEasyData = await resp.json()
@@ -205,7 +211,8 @@ export default {
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug submitter_slug: this.submitterSlug
}), }),
headers: { 'Content-Type': 'application/json' } ...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}) })
if (resp.status === 404) { if (resp.status === 404) {
+1 -7
View File
@@ -110,7 +110,7 @@
data-turbo="false" data-turbo="false"
class="inline" class="inline"
method="post" method="post"
:action="`/d/${template.slug}`" :action="`/start_form_self/${template.id}`"
@submit="maybeShowErrorTemplateAlert" @submit="maybeShowErrorTemplateAlert"
> >
<input <input
@@ -125,12 +125,6 @@
:value="authenticityToken" :value="authenticityToken"
autocomplete="off" autocomplete="off"
> >
<input
type="hidden"
name="selfsign"
value="true"
autocomplete="off"
>
<button <button
class="btn btn-primary btn-ghost text-base hidden md:flex" class="btn btn-primary btn-ghost text-base hidden md:flex"
type="submit" type="submit"
@@ -417,10 +417,8 @@ function buildOperators (tags) {
break break
} }
case 'end': { case 'end': {
const popped = stack.pop() if (stack.length > 1) {
stack.pop().operator.endTag = tag
if (popped.operator) {
popped.operator.endTag = tag
} }
break break
@@ -434,7 +432,13 @@ function buildOperators (tags) {
return operators return operators
} }
function hasBlankKey (keyString) {
return !keyString || keyString.split('.').some((key) => !key.trim())
}
function assignNestedSchema (propertiesHash, parentProperties, keyString, value) { function assignNestedSchema (propertiesHash, parentProperties, keyString, value) {
if (hasBlankKey(keyString)) return
const keys = keyString.split('.') const keys = keyString.split('.')
const lastKey = keys.pop() const lastKey = keys.pop()
@@ -460,6 +464,8 @@ function assignNestedSchema (propertiesHash, parentProperties, keyString, value)
} }
function assignNestedSchemaWithPriority (propertiesHash, parentProperties, keyString, newType) { function assignNestedSchemaWithPriority (propertiesHash, parentProperties, keyString, newType) {
if (hasBlankKey(keyString)) return
const keys = keyString.split('.') const keys = keyString.split('.')
const lastKey = keys.pop() const lastKey = keys.pop()
@@ -523,6 +529,8 @@ function processOperators (operators, propertiesHash = {}, parentProperties = {}
processOperators(op.elseChildren, propertiesHash, parentProperties) processOperators(op.elseChildren, propertiesHash, parentProperties)
break break
case 'for': { case 'for': {
if (hasBlankKey(op.variableName)) break
const parts = op.variableName.split('.') const parts = op.variableName.split('.')
const singularKey = singularize(parts[parts.length - 1]) const singularKey = singularize(parts[parts.length - 1])
-17
View File
@@ -1,17 +0,0 @@
# frozen_string_literal: true
class GeneratePreviewImagesJob
include Sidekiq::Job
sidekiq_options queue: :images
def perform(params = {})
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, 1..max_page,
concurrency: 1)
end
end
+13 -9
View File
@@ -123,13 +123,18 @@ class ProcessSubmitterCompletionJob
user = submission.created_by_user || template.author user = submission.created_by_user || template.author
copy_email_configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
is_copy_email_enabled = documents_copy_email_enabled?(submitter, copy_email_configs)
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false && if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
(!template || template.preferences['completed_notification_email_enabled'] != false) (!template || template.preferences['completed_notification_email_enabled'] != false)
user_submitter = submission.submitters.find { |s| s.email == user.email } user_submitter = submission.submitters.find { |s| s.email == user.email }
is_sent_to_user = is_sent_to_user =
if user.role != 'integration' && if user.role != 'integration' &&
(!user_submitter || user_submitter.preferences['send_email'] == false) && (!user_submitter || user_submitter.preferences['send_email'] == false || !is_copy_email_enabled) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
SubmitterMailer.completed_email(submitter, user).deliver_later! SubmitterMailer.completed_email(submitter, user).deliver_later!
@@ -139,11 +144,11 @@ class ProcessSubmitterCompletionJob
enqueue_bcc_completed_emails(submitter, user, is_sent_to_user) enqueue_bcc_completed_emails(submitter, user, is_sent_to_user)
end end
maybe_enqueue_copy_emails(submitter) enqueue_copy_emails(submitter, copy_email_configs) if is_copy_email_enabled
end end
def enqueue_bcc_completed_emails(submitter, user, is_sent_to_user) def enqueue_bcc_completed_emails(submitter, user, is_sent_to_user)
bcc_addresses = build_bcc_addresses(submitter.submission) bcc_addresses = build_bcc_addresses(submitter.submission).uniq
raise TooManyBcc, submitter.account_id if Docuseal.multitenant? && bcc_addresses.size > BCC_LIMIT raise TooManyBcc, submitter.account_id if Docuseal.multitenant? && bcc_addresses.size > BCC_LIMIT
@@ -154,14 +159,13 @@ class ProcessSubmitterCompletionJob
end end
end end
def maybe_enqueue_copy_emails(submitter) def documents_copy_email_enabled?(submitter, configs)
return if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false return false if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false
configs = AccountConfigs.find_or_initialize_for_key(submitter.account, configs.value['enabled'] != false
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) end
return if configs.value['enabled'] == false
def enqueue_copy_emails(submitter, configs)
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false } to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by { |e| e.completed_at || Time.current }.select(&:email?).map(&:friendly_name) .sort_by { |e| e.completed_at || Time.current }.select(&:email?).map(&:friendly_name)
+2 -1
View File
@@ -112,7 +112,8 @@ class Submission < ApplicationRecord
api: 'api', api: 'api',
embed: 'embed', embed: 'embed',
mcp: 'mcp', mcp: 'mcp',
link: 'link' link: 'link',
self: 'self'
}, scope: false, prefix: true }, scope: false, prefix: true
enum :submitters_order, { enum :submitters_order, {
@@ -21,13 +21,15 @@
<% end %> <% end %>
</div> </div>
<%= render 'bcc_form', config: @bcc_config %> <%= render 'bcc_form', config: @bcc_config %>
<div class="flex justify-between items-end mb-4 mt-8"> <% unless can?(:manage, :personalization_restricted) %>
<h2 class="text-2xl md:text-3xl font-bold"> <div class="flex justify-between items-end mb-4 mt-8">
<%= t('sign_request_email_reminders') %> <h2 class="text-2xl md:text-3xl font-bold">
</h2> <%= t('sign_request_email_reminders') %>
</div> </h2>
<%= render 'reminder_banner' %> </div>
<%= render 'reminder_form', config: @reminder_config %> <%= render 'reminder_banner' %>
<%= render 'reminder_form', config: @reminder_config %>
<% end %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
</div> </div>
@@ -10,7 +10,12 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
@@ -10,7 +10,12 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
@@ -12,7 +12,7 @@
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>"> <span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %> <%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
+2 -2
View File
@@ -2,7 +2,7 @@
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<%= render 'banner' %> <%= render 'start_form/banner' %>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center"> <div class="flex items-center">
@@ -26,7 +26,7 @@
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %> <%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %> <% if @template.shared_link? && Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %>
</toggle-submit> </toggle-submit>
@@ -23,12 +23,12 @@
<div> <div>
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %> <%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
</div> </div>
<%= form_for '', url: start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %> <%= form_for '', url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0"> <div dir="auto" class="form-control !mt-0">
<%= f.hidden_field 'resubmit', value: params[:resubmit] %> <%= f.hidden_field 'resubmit', value: params[:resubmit] %>
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %> <%= f.hidden_field 'submitter[name]', value: params[:name] || params.dig(:submitter, :name) %>
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %> <%= f.hidden_field 'submitter[email]', value: params[:email] || params.dig(:submitter, :email) %>
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %> <%= f.hidden_field 'submitter[phone]', value: params[:phone] || params.dig(:submitter, :phone) %>
<%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %> <%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
<div class="flex justify-between items-center mt-1"> <div class="flex justify-between items-center mt-1">
<span> <span>
@@ -49,6 +49,10 @@
<%= f.button button_title(title: t('submit')), class: 'base-button' %> <%= f.button button_title(title: t('submit')), class: 'base-button' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %> <% if @resubmit_submitter %>
<%= button_to t(:re_send_email), resubmit_form_path, params: { resubmit: @resubmit_submitter.slug }, method: :put, id: 'resend_code', class: 'hidden' %>
<% else %>
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
<% end %>
</div> </div>
</main> </main>
+1 -1
View File
@@ -4,7 +4,7 @@
<% end %> <% end %>
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4"> <main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'start_form/banner' %>
<p class="text-xl font-semibold text-center"> <p class="text-xl font-semibold text-center">
<%= t('share_link_is_currently_disabled') %> <%= t('share_link_is_currently_disabled') %>
</p> </p>
+3 -2
View File
@@ -6,7 +6,7 @@
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'start_form/banner' %>
<% unless @template.archived_at? %> <% unless @template.archived_at? %>
<p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p> <p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p>
<% end %> <% end %>
@@ -28,7 +28,8 @@
</div> </div>
</div> </div>
<% if !@template.archived_at? && !@template.account.archived_at? %> <% if !@template.archived_at? && !@template.account.archived_at? %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %> <%= form_for @submitter, url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<%= hidden_field_tag :resubmit, @resubmit_submitter.slug if @resubmit_submitter %>
<% if @error_message %> <% if @error_message %>
<div class="alert items-start bg-base-100 border border-red-400 text-red-400"> <div class="alert items-start bg-base-100 border border-red-400 text-red-400">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %> <%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
@@ -69,7 +69,7 @@
<%= f.label :message, t('body'), class: 'label' %> <%= f.label :message, t('body'), class: 'label' %>
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %> <% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
<%= render 'personalization_settings/email_body_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_edit_viewer ? view_template_body : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || (is_edit_viewer ? view_config.value['body'] : default_body), variables: body_variables %> <%= render 'personalization_settings/email_body_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_edit_viewer ? view_template_body : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || (is_edit_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
<% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' %> <% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' && template && can?(:update, template) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer"> <label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %> <%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
<span class="label"><%= t('save_as_default_template_message') %></span> <span class="label"><%= t('save_as_default_template_message') %></span>
@@ -1,5 +1,7 @@
<% path_uri = Addressable::URI.parse(params[:path].to_s) %>
<% path = url_for(**Rails.application.routes.recognize_path(path_uri.path, method: :get)) %>
<%= render 'shared/turbo_modal', title: local_assigns[:title] do %> <%= render 'shared/turbo_modal', title: local_assigns[:title] do %>
<%= form_for '', url: params[:path], method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> <%= form_for '', url: path, method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
<%= hidden_field_tag :q, params[:q] if params[:q].present? %> <%= hidden_field_tag :q, params[:q] if params[:q].present? %>
<% local_assigns[:default_params].each do |key, value| %> <% local_assigns[:default_params].each do |key, value| %>
<%= hidden_field_tag(key, value) if value.present? %> <%= hidden_field_tag(key, value) if value.present? %>
@@ -10,7 +12,7 @@
</div> </div>
<% if params[:with_remove] %> <% if params[:with_remove] %>
<div class="text-center w-full mt-4"> <div class="text-center w-full mt-4">
<%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %> <%= link_to t('remove_filter'), "#{path}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
+1 -1
View File
@@ -44,7 +44,7 @@
<% end %> <% end %>
</div> </div>
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template_submitters) %> <% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template_submitters) %>
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && @submitter.completed_at? && @submitter.completed_at > 14.days.ago && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @submitter.template && !@submitter.template.archived_at? %> <% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && Submitters::StartForm.can_resubmit?(@submitter) && @submitter.template && !@submitter.template.archived_at? %>
<div class="divider uppercase"><%= t('or') %></div> <div class="divider uppercase"><%= t('or') %></div>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
+1 -1
View File
@@ -99,7 +99,7 @@
</open-modal-mobile> </open-modal-mobile>
<% end %> <% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && @template.variables_schema.blank? %> <% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && @template.variables_schema.blank? %>
<%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %> <%= button_to start_form_self_path(@template), method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
<%= svg_icon('writing', class: 'w-6 h-6') %> <%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span> <span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %> <% end %>
@@ -10,7 +10,12 @@
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body']) do |ff| %> <%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body']) do |ff| %>
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %> <div class="flex items-center">
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<% if is_custom_template_email %> <% if is_custom_template_email %>
<label for="submitter_completed_email_reset_link" class="label underline"> <label for="submitter_completed_email_reset_link" class="label underline">
<%= t('reset_default') %> <%= t('reset_default') %>
@@ -10,7 +10,12 @@
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body']) do |ff| %> <%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body']) do |ff| %>
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %> <div class="flex items-center">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<% if is_custom_template_email %> <% if is_custom_template_email %>
<label for="submitter_documents_copy_email_reset_link" class="label underline"> <label for="submitter_documents_copy_email_reset_link" class="label underline">
<%= t('reset_default') %> <%= t('reset_default') %>
@@ -16,7 +16,12 @@
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(template_email_preferences_values.presence || default_template_email_preferences_values)) do |ff| %> <%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(template_email_preferences_values.presence || default_template_email_preferences_values)) do |ff| %>
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %> <div class="flex items-center">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<% if is_custom_template_email %> <% if is_custom_template_email %>
<label for="submitter_invitation_email_reset_link" class="label underline"> <label for="submitter_invitation_email_reset_link" class="label underline">
<%= t('reset_default') %> <%= t('reset_default') %>
@@ -10,7 +10,12 @@
<%= f.fields_for :preferences, Struct.new(:invitation_view_email_subject, :invitation_view_email_body).new(@template.preferences['invitation_view_email_subject'].presence || configs['subject'], @template.preferences['invitation_view_email_body'].presence || configs['body']) do |ff| %> <%= f.fields_for :preferences, Struct.new(:invitation_view_email_subject, :invitation_view_email_body).new(@template.preferences['invitation_view_email_subject'].presence || configs['subject'], @template.preferences['invitation_view_email_body'].presence || configs['body']) do |ff| %>
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %> <div class="flex items-center">
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('view_only_email_sent_to_recipients_that_need_to_only_review_the_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<% if is_custom_template_email %> <% if is_custom_template_email %>
<label for="submitter_view_invitation_email_reset_link" class="label underline"> <label for="submitter_view_invitation_email_reset_link" class="label underline">
<%= t('reset_default') %> <%= t('reset_default') %>
+13 -26
View File
@@ -1,7 +1,7 @@
<div id="result"> <div id="result">
<% files.zip(pdfs).each do |file, pdf| %> <% files.zip(signatures).each do |file, file_signatures| %>
<div class="mb-4 border border-base-300 rounded-md py-2 px-3"> <div class="mb-4 border border-base-300 rounded-md py-2 px-3">
<% if pdf.signatures.to_a.size == 0 %> <% if file_signatures.size == 0 %>
<div class="text-sm"> <div class="text-sm">
<%= file.original_filename %> <%= file.original_filename %>
</div> </div>
@@ -11,42 +11,29 @@
<% else %> <% else %>
<div class="flex items-center space-x-1 border-b border-dashed border-base-300 pb-2"> <div class="flex items-center space-x-1 border-b border-dashed border-base-300 pb-2">
<%= svg_icon('file_text', class: 'w-5 h-5 inline') %> <%= svg_icon('file_text', class: 'w-5 h-5 inline') %>
<span><%= file.original_filename %> - <%= pluralize(pdf.signatures.to_a.size, 'Signature') %></span> <span><%= file.original_filename %> - <%= pluralize(file_signatures.size, 'Signature') %></span>
</div> </div>
<% pdf.signatures.to_a.each do |signature| %> <% file_signatures.each do |signature| %>
<div class="mt-3"> <div class="mt-3">
<div class="space-y-1 font-medium pb-2 text-xl"> <div class="space-y-1 font-medium pb-2 text-xl">
<% signature.verify(trusted_certs:).messages.map(&:content).each do |message| %> <% signature.messages.each do |message| %>
<p class="flex space-x-1 items-center"> <p class="flex space-x-1 items-center">
<% if message == 'Signature verification failed' %> <% if message.status == :error %>
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %> <%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
<% elsif message == 'Signature valid' %> <% elsif message.status == :success %>
<%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %> <%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %>
<% elsif message.status == :warning %>
<%= svg_icon('alert_circle', class: 'w-6 h-6 text-yellow-500') %>
<% end %> <% end %>
<span> <span>
<%= message %> <%= message.text %>
</span> </span>
</p> </p>
<% if message == 'Signature valid' %>
<p class="flex space-x-1 items-center">
<% if trusted_certs.any? { |e| e.public_key.to_der == signature.signature_handler.signer_certificate.public_key.to_der } %>
<%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %>
<span>
<%= t('signed_with_trusted_certificate') %>
</span>
<% else %>
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
<span>
<%= t('signed_with_external_certificate') %>
</span>
<% end %>
</p>
<% end %>
<% end %> <% end %>
</div> </div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('user', class: 'w-5 h-5 inline') %> <%= svg_icon('user', class: 'w-5 h-5 inline') %>
<span><%= signature.signing_reason %></span> <span><%= signature.reason %></span>
</div> </div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('calendar', class: 'w-5 h-5 inline') %> <%= svg_icon('calendar', class: 'w-5 h-5 inline') %>
@@ -54,11 +41,11 @@
</div> </div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %> <%= svg_icon('certificate', class: 'w-5 h-5 inline') %>
<span><%= signature.signer_name.force_encoding('UTF-8') %></span> <span><%= signature.common_name %></span>
</div> </div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('lock_access', class: 'w-5 h-5 inline') %> <%= svg_icon('lock_access', class: 'w-5 h-5 inline') %>
<span><%= signature.signature_type %></span> <span><%= signature.type %></span>
</div> </div>
</div> </div>
<% end %> <% end %>
+2
View File
@@ -19,6 +19,8 @@ module DocuSeal
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 8.1 config.load_defaults 8.1
config.active_support.message_serializer = :json
config.autoload_lib(ignore: %w[assets tasks puma]) config.autoload_lib(ignore: %w[assets tasks puma])
config.active_storage.routes_prefix = '' config.active_storage.routes_prefix = ''
+63
View File
@@ -110,4 +110,67 @@ module HexaPDF
end end
end end
end end
module CycleSafeInheritedValue
using HexaPDF::Type::AcroForm::Field::HashRefinement
def inherited_value(field, name)
seen = Set.new.compare_by_identity
seen << field.value
while field.value[name].nil? && (parent = field[:Parent]) && seen.add?(parent.value)
field = parent
end
field.value[name].nil? ? nil : field[name]
end
end
module CycleSafeEachField
def each_field(terminal_only: true)
return to_enum(__method__, terminal_only:) unless block_given?
seen = Set.new.compare_by_identity
process_field_array = lambda do |array|
array.each_with_index do |field, index|
next if field.nil?
unless field.respond_to?(:type) && field.type == :XXAcroFormField
array[index] = field = HexaPDF::Type::AcroForm::Field.wrap(document, field)
end
next unless seen.add?(field.value)
if field.terminal_field?
yield(field)
else
yield(field) unless terminal_only
process_field_array.call(field[:Kids])
end
end
end
process_field_array.call(root_fields)
self
end
end
module CycleSafeFullFieldName
def full_field_name(seen = Set.new.compare_by_identity)
return field_name unless seen.add?(value)
if key?(:Parent)
[self[:Parent].full_field_name(seen), field_name].compact.join('.')
else
field_name
end
end
end
end end
HexaPDF::Type::AcroForm::Field.singleton_class.prepend(HexaPDF::CycleSafeInheritedValue)
HexaPDF::Type::AcroForm::Field.prepend(HexaPDF::CycleSafeFullFieldName)
HexaPDF::Type::AcroForm::Form.prepend(HexaPDF::CycleSafeEachField)
+396 -28
View File
@@ -57,8 +57,11 @@ en: &en
paypal_account_has_been_connected: PayPal account has been connected. paypal_account_has_been_connected: PayPal account has been connected.
re_connect_paypal: Re-connect PayPal re_connect_paypal: Re-connect PayPal
disconnect: Disconnect disconnect: Disconnect
failed_to_connect_paypal: Failed to connect PayPal. Please try again. failed_to_connect_paypal_please_try_again: Failed to connect PayPal. Please try again.
paypal_connect_not_completed: PayPal connection was not completed. paypal_connection_was_not_completed: PayPal connection was not completed.
disconnecting_your_paypal_account_will_prevent_you_from_offering_paypal_services_and_products_on_your_website_do_you_wish_to_continue: Disconnecting your PayPal account will prevent you from offering PayPal services and products on your website. Do you wish to continue?
attention_you_currently_cannot_receive_payments_due_to_restriction_on_your_paypal_account_please_reach_out_to_paypal_customer_support_or_connect_to_paypal_com_for_more_information: 'Attention: You currently cannot receive payments due to restriction on your PayPal account. Please reach out to PayPal Customer Support or connect to https://www.paypal.com for more information.'
attention_please_confirm_your_email_address_on_paypal_com_in_order_to_receive_payments_you_currently_cannot_receive_payments: 'Attention: Please confirm your email address on https://www.paypal.com/businessprofile/settings in order to receive payments! You currently cannot receive payments.'
payment_receipt: Payment Receipt payment_receipt: Payment Receipt
amount_paid: Amount paid amount_paid: Amount paid
transaction: Transaction transaction: Transaction
@@ -225,6 +228,7 @@ en: &en
profile_details: Profile Details profile_details: Profile Details
sign_up_with_google: Sign up with Google sign_up_with_google: Sign up with Google
sign_up_with_microsoft: Sign up with Microsoft sign_up_with_microsoft: Sign up with Microsoft
redirecting_to_provider: Redirecting to %{provider}...
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.com/terms">Terms of Service</a>.' by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.com/terms">Terms of Service</a>.'
enter_email_to_continue: Enter email to continue enter_email_to_continue: Enter email to continue
account: Account account: Account
@@ -593,6 +597,9 @@ en: &en
there_are_no_signatures: There are no signatures there_are_no_signatures: There are no signatures
signed_with_trusted_certificate: Signed with trusted certificate signed_with_trusted_certificate: Signed with trusted certificate
signed_with_external_certificate: Signed with external certificate signed_with_external_certificate: Signed with external certificate
signature_valid: Signature valid
signature_verification_failed: Signature verification failed
contains_unsigned_changes_after_the_last_signature: Contains unsigned changes after the last signature
setup_2fa_to_continue: Setup 2FA to continue. setup_2fa_to_continue: Setup 2FA to continue.
create_a_new_account: Create a new account. create_a_new_account: Create a new account.
you_are_already_signed_in: You are already signed in. you_are_already_signed_in: You are already signed in.
@@ -985,6 +992,13 @@ en: &en
open_file_from: 'Open file from' open_file_from: 'Open file from'
cancel: Cancel cancel: Cancel
open: Open open: Open
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: Signature request email sent to recipients with a link to review and sign.
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: View only email sent to recipients that need to only review the documents.
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Reminds recipients who haven't completed their part yet to sign, following the reminders schedule.
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: Signature request SMS sent to recipients with a link to review and sign.
phone_2fa_verification_text_message: Phone 2FA verification text message.
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Notification email sent to the sender user and BCC addresses once the submission is completed by all parties.
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: Completed documents copy email sent to recipients upon completion containing their documents.
devise: devise:
confirmations: confirmations:
confirmed: Your email address has been successfully confirmed. confirmed: Your email address has been successfully confirmed.
@@ -1000,6 +1014,7 @@ en: &en
invite: Invite invite: Invite
link: Link link: Link
mcp: MCP mcp: MCP
self: Sign Yourself
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}' send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
bounce_email_html: '<b>Email bounced</b> %{submitter_name}' bounce_email_html: '<b>Email bounced</b> %{submitter_name}'
@@ -1113,9 +1128,16 @@ en: &en
submission_submitters: Submitters list submission_submitters: Submitters list
submission_link: Submission link submission_link: Submission link
documents_link: Documents link documents_link: Documents link
date:
formats:
default: "%m/%d/%Y"
short: "%b %-d"
long: "%B %-d, %Y"
time: time:
formats: formats:
detailed: "%B %d, %Y %H:%M:%S" short: "%b %-d, %-I:%M %p"
long: "%B %-d, %Y at %-I:%M %p"
detailed: "%B %-d, %Y at %-I:%M:%S %p"
es: &es es: &es
knowledge_based_authentication: Autenticación basada en el conocimiento knowledge_based_authentication: Autenticación basada en el conocimiento
@@ -1147,8 +1169,11 @@ es: &es
paypal_integration: Integración con PayPal paypal_integration: Integración con PayPal
paypal_account_has_been_connected: La cuenta de PayPal ha sido conectada. paypal_account_has_been_connected: La cuenta de PayPal ha sido conectada.
re_connect_paypal: Volver a conectar PayPal re_connect_paypal: Volver a conectar PayPal
failed_to_connect_paypal: No se pudo conectar PayPal. Por favor, inténtalo de nuevo. failed_to_connect_paypal_please_try_again: No se pudo conectar PayPal. Por favor, inténtalo de nuevo.
paypal_connect_not_completed: La conexión con PayPal no se completó. paypal_connection_was_not_completed: La conexión con PayPal no se completó.
disconnecting_your_paypal_account_will_prevent_you_from_offering_paypal_services_and_products_on_your_website_do_you_wish_to_continue: Desconectar tu cuenta de PayPal impedirá que ofrezcas servicios y productos de PayPal en tu sitio web. ¿Deseas continuar?
attention_you_currently_cannot_receive_payments_due_to_restriction_on_your_paypal_account_please_reach_out_to_paypal_customer_support_or_connect_to_paypal_com_for_more_information: 'Atención: Actualmente no puedes recibir pagos debido a una restricción en tu cuenta de PayPal. Por favor, contacta con el Soporte al Cliente de PayPal o visita https://www.paypal.com para más información.'
attention_please_confirm_your_email_address_on_paypal_com_in_order_to_receive_payments_you_currently_cannot_receive_payments: 'Atención: Por favor, confirma tu dirección de correo electrónico en https://www.paypal.com/businessprofile/settings para poder recibir pagos. Actualmente no puedes recibir pagos.'
payment_receipt: Recibo de pago payment_receipt: Recibo de pago
amount_paid: Importe pagado amount_paid: Importe pagado
transaction: Transacción transaction: Transacción
@@ -1325,6 +1350,7 @@ es: &es
profile_details: Detalles del perfil profile_details: Detalles del perfil
sign_up_with_google: Registrarse con Google sign_up_with_google: Registrarse con Google
sign_up_with_microsoft: Registrarse con Microsoft sign_up_with_microsoft: Registrarse con Microsoft
redirecting_to_provider: Redirigiendo a %{provider}...
by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.com/terms">Términos de Servicio</a>.' by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.com/terms">Términos de Servicio</a>.'
enter_email_to_continue: Ingresa tu correo electrónico para continuar enter_email_to_continue: Ingresa tu correo electrónico para continuar
account: Cuenta account: Cuenta
@@ -1452,6 +1478,13 @@ es: &es
completed_form_message: Mensaje de formulario completado completed_form_message: Mensaje de formulario completado
completed_form_redirect_button: Botón de redirección de formulario completado completed_form_redirect_button: Botón de redirección de formulario completado
documents_copy_email: Correo de copia de documentos documents_copy_email: Correo de copia de documentos
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: Correo de solicitud de firma enviado a los destinatarios con un enlace para revisar y firmar.
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: Correo de solo lectura enviado a los destinatarios que solo necesitan revisar los documentos.
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Recuerda firmar a los destinatarios que aún no han completado su parte, según la programación de recordatorios.
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS de solicitud de firma enviado a los destinatarios con un enlace para revisar y firmar.
phone_2fa_verification_text_message: Mensaje de texto de verificación 2FA por teléfono.
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Correo de notificación enviado al usuario remitente y a las direcciones BCC una vez que todas las partes han completado el envío.
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: Correo con la copia de los documentos completados enviado a los destinatarios al finalizar, con sus documentos.
form_custom_css: CSS personalizado del formulario form_custom_css: CSS personalizado del formulario
subject: Asunto subject: Asunto
body: Cuerpo body: Cuerpo
@@ -1693,6 +1726,9 @@ es: &es
there_are_no_signatures: No hay firmas there_are_no_signatures: No hay firmas
signed_with_trusted_certificate: Firmado con certificado de confianza signed_with_trusted_certificate: Firmado con certificado de confianza
signed_with_external_certificate: Firmado con certificado externo signed_with_external_certificate: Firmado con certificado externo
signature_valid: Firma válida
signature_verification_failed: Verificación de firma fallida
contains_unsigned_changes_after_the_last_signature: Contiene cambios no firmados después de la última firma
setup_2fa_to_continue: Configura 2FA para continuar. setup_2fa_to_continue: Configura 2FA para continuar.
create_a_new_account: Crear una nueva cuenta. create_a_new_account: Crear una nueva cuenta.
you_are_already_signed_in: Ya has iniciado sesión. you_are_already_signed_in: Ya has iniciado sesión.
@@ -2097,6 +2133,7 @@ es: &es
invite: Invitación invite: Invitación
link: Enlace link: Enlace
mcp: MCP mcp: MCP
self: Firma tú mismo
submission_event_names: submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}' send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}' bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}'
@@ -2210,9 +2247,30 @@ es: &es
submission_submitters: Lista de firmantes submission_submitters: Lista de firmantes
submission_link: Enlace del envío submission_link: Enlace del envío
documents_link: Enlace de los documentos documents_link: Enlace de los documentos
date:
abbr_month_names:
-
- ene
- feb
- mar
- abr
- may
- jun
- jul
- ago
- sept
- oct
- nov
- dic
formats:
default: "%d/%m/%Y"
short: "%-d %b"
long: "%-d de %B de %Y"
time: time:
formats: formats:
detailed: "%-d de %B de %Y %H:%M:%S" short: "%-d %b, %-H:%M"
long: "%-d de %B de %Y a las %-H:%M"
detailed: "%-d de %B de %Y a las %-H:%M:%S"
it: &it it: &it
knowledge_based_authentication: Autenticazione basata sulla conoscenza knowledge_based_authentication: Autenticazione basata sulla conoscenza
@@ -2244,8 +2302,11 @@ it: &it
paypal_integration: Integrazione PayPal paypal_integration: Integrazione PayPal
paypal_account_has_been_connected: L'account PayPal è stato collegato. paypal_account_has_been_connected: L'account PayPal è stato collegato.
re_connect_paypal: Ricollega PayPal re_connect_paypal: Ricollega PayPal
failed_to_connect_paypal: Impossibile collegare PayPal. Riprova. failed_to_connect_paypal_please_try_again: Impossibile collegare PayPal. Riprova.
paypal_connect_not_completed: La connessione con PayPal non è stata completata. paypal_connection_was_not_completed: La connessione con PayPal non è stata completata.
disconnecting_your_paypal_account_will_prevent_you_from_offering_paypal_services_and_products_on_your_website_do_you_wish_to_continue: La disconnessione del tuo account PayPal ti impedirà di offrire servizi e prodotti PayPal sul tuo sito web. Vuoi continuare?
attention_you_currently_cannot_receive_payments_due_to_restriction_on_your_paypal_account_please_reach_out_to_paypal_customer_support_or_connect_to_paypal_com_for_more_information: 'Attenzione: Al momento non puoi ricevere pagamenti a causa di una restrizione sul tuo account PayPal. Contatta l''Assistenza Clienti PayPal o visita https://www.paypal.com per maggiori informazioni.'
attention_please_confirm_your_email_address_on_paypal_com_in_order_to_receive_payments_you_currently_cannot_receive_payments: 'Attenzione: Conferma il tuo indirizzo email su https://www.paypal.com/businessprofile/settings per poter ricevere pagamenti! Al momento non puoi ricevere pagamenti.'
payment_receipt: Ricevuta di pagamento payment_receipt: Ricevuta di pagamento
amount_paid: Importo pagato amount_paid: Importo pagato
transaction: Transazione transaction: Transazione
@@ -2422,6 +2483,7 @@ it: &it
profile_details: Dettagli del profilo profile_details: Dettagli del profilo
sign_up_with_google: Registrati con Google sign_up_with_google: Registrati con Google
sign_up_with_microsoft: Registrati con Microsoft sign_up_with_microsoft: Registrati con Microsoft
redirecting_to_provider: Reindirizzamento a %{provider}...
by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.com/terms">Termini di servizio</a>.' by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.com/terms">Termini di servizio</a>.'
enter_email_to_continue: "Inserisci l'email per continuare" enter_email_to_continue: "Inserisci l'email per continuare"
account: Account account: Account
@@ -2549,6 +2611,13 @@ it: &it
completed_form_message: Messaggio di completamento del modulo completed_form_message: Messaggio di completamento del modulo
completed_form_redirect_button: Pulsante di reindirizzamento dopo il completamento del modulo completed_form_redirect_button: Pulsante di reindirizzamento dopo il completamento del modulo
documents_copy_email: Email con copia dei documenti documents_copy_email: Email con copia dei documenti
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: Email di richiesta di firma inviata ai destinatari con un link per revisionare e firmare.
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: Email di sola lettura inviata ai destinatari che devono solo revisionare i documenti.
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Ricorda di firmare ai destinatari che non hanno ancora completato la loro parte, in base alla pianificazione dei promemoria.
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS di richiesta di firma inviato ai destinatari con un link per revisionare e firmare.
phone_2fa_verification_text_message: Messaggio di testo per la verifica 2FA tramite telefono.
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Email di notifica inviata all'utente mittente e agli indirizzi BCC una volta che l'invio è stato completato da tutte le parti.
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: Email con la copia dei documenti completati inviata ai destinatari al completamento, con i loro documenti.
form_custom_css: CSS personalizzato del modulo form_custom_css: CSS personalizzato del modulo
subject: Oggetto subject: Oggetto
body: Corpo body: Corpo
@@ -2790,6 +2859,9 @@ it: &it
there_are_no_signatures: Non ci sono firme there_are_no_signatures: Non ci sono firme
signed_with_trusted_certificate: Firmato con certificato affidabile signed_with_trusted_certificate: Firmato con certificato affidabile
signed_with_external_certificate: Firmato con certificato esterno signed_with_external_certificate: Firmato con certificato esterno
signature_valid: Firma valida
signature_verification_failed: Verifica della firma non riuscita
contains_unsigned_changes_after_the_last_signature: Contiene modifiche non firmate dopo l’ultima firma
setup_2fa_to_continue: Configura 2FA per continuare. setup_2fa_to_continue: Configura 2FA per continuare.
create_a_new_account: Crea un nuovo account. create_a_new_account: Crea un nuovo account.
you_are_already_signed_in: "Hai già effettuato l'accesso." you_are_already_signed_in: "Hai già effettuato l'accesso."
@@ -3194,6 +3266,7 @@ it: &it
invite: Invito invite: Invito
link: Link link: Link
mcp: MCP mcp: MCP
self: Firma te stesso
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email inviata</b> a %{submitter_name}' send_email_to_html: '<b>Email inviata</b> a %{submitter_name}'
bounce_email_html: '<b>Email respinta</b> %{submitter_name}' bounce_email_html: '<b>Email respinta</b> %{submitter_name}'
@@ -3307,9 +3380,16 @@ it: &it
submission_submitters: Lista dei firmatari submission_submitters: Lista dei firmatari
submission_link: Link dell'invio submission_link: Link dell'invio
documents_link: Link dei documenti documents_link: Link dei documenti
date:
formats:
default: "%d/%m/%Y"
short: "%-d %b"
long: "%-d %B %Y"
time: time:
formats: formats:
detailed: "%d %B %Y %H:%M:%S" short: "%-d %b, %H:%M"
long: "%-d %B %Y alle ore %H:%M"
detailed: "%-d %B %Y alle ore %H:%M:%S"
fr: &fr fr: &fr
knowledge_based_authentication: Authentification basée sur la connaissance knowledge_based_authentication: Authentification basée sur la connaissance
@@ -3341,8 +3421,11 @@ fr: &fr
paypal_integration: Intégration PayPal paypal_integration: Intégration PayPal
paypal_account_has_been_connected: Le compte PayPal a été connecté. paypal_account_has_been_connected: Le compte PayPal a été connecté.
re_connect_paypal: Reconnecter PayPal re_connect_paypal: Reconnecter PayPal
failed_to_connect_paypal: Échec de la connexion à PayPal. Veuillez réessayer. failed_to_connect_paypal_please_try_again: Échec de la connexion à PayPal. Veuillez réessayer.
paypal_connect_not_completed: La connexion à PayPal n'a pas été complétée. paypal_connection_was_not_completed: La connexion à PayPal n'a pas été complétée.
disconnecting_your_paypal_account_will_prevent_you_from_offering_paypal_services_and_products_on_your_website_do_you_wish_to_continue: Déconnecter votre compte PayPal vous empêchera de proposer des services et produits PayPal sur votre site web. Voulez-vous continuer ?
attention_you_currently_cannot_receive_payments_due_to_restriction_on_your_paypal_account_please_reach_out_to_paypal_customer_support_or_connect_to_paypal_com_for_more_information: 'Attention : Vous ne pouvez actuellement pas recevoir de paiements en raison d''une restriction sur votre compte PayPal. Veuillez contacter le service client PayPal ou visiter https://www.paypal.com pour plus d''informations.'
attention_please_confirm_your_email_address_on_paypal_com_in_order_to_receive_payments_you_currently_cannot_receive_payments: 'Attention : Veuillez confirmer votre adresse e-mail sur https://www.paypal.com/businessprofile/settings afin de pouvoir recevoir des paiements ! Vous ne pouvez actuellement pas recevoir de paiements.'
payment_receipt: Reçu de paiement payment_receipt: Reçu de paiement
amount_paid: Montant payé amount_paid: Montant payé
transaction: Transaction transaction: Transaction
@@ -3519,6 +3602,7 @@ fr: &fr
profile_details: Détails du profil profile_details: Détails du profil
sign_up_with_google: S’inscrire avec Google sign_up_with_google: S’inscrire avec Google
sign_up_with_microsoft: S’inscrire avec Microsoft sign_up_with_microsoft: S’inscrire avec Microsoft
redirecting_to_provider: Redirection vers %{provider}...
by_creating_an_account_you_agree_to_our_html: En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.com/privacy">Politique de confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.com/terms">Conditions d’utilisation</a>. by_creating_an_account_you_agree_to_our_html: En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.com/privacy">Politique de confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.com/terms">Conditions d’utilisation</a>.
enter_email_to_continue: Entrez votre e‑mail pour continuer enter_email_to_continue: Entrez votre e‑mail pour continuer
account: Compte account: Compte
@@ -3646,6 +3730,13 @@ fr: &fr
completed_form_message: Message de formulaire complété completed_form_message: Message de formulaire complété
completed_form_redirect_button: Bouton de redirection après finalisation completed_form_redirect_button: Bouton de redirection après finalisation
documents_copy_email: E‑mail avec copie des documents documents_copy_email: E‑mail avec copie des documents
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-mail de demande de signature envoyé aux destinataires avec un lien pour consulter et signer.
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-mail en lecture seule envoyé aux destinataires qui doivent uniquement consulter les documents.
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Rappelle de signer aux destinataires qui n'ont pas encore terminé leur partie, selon la planification des rappels.
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS de demande de signature envoyé aux destinataires avec un lien pour consulter et signer.
phone_2fa_verification_text_message: Message texte de vérification 2FA par téléphone.
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: E-mail de notification envoyé à l'utilisateur expéditeur et aux adresses Cci une fois la soumission complétée par toutes les parties.
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-mail avec la copie des documents finalisés, envoyé aux destinataires à la finalisation, contenant leurs documents.
form_custom_css: CSS personnalisé du formulaire form_custom_css: CSS personnalisé du formulaire
subject: Objet subject: Objet
body: Corps body: Corps
@@ -3887,6 +3978,9 @@ fr: &fr
there_are_no_signatures: Il n’y a pas de signatures there_are_no_signatures: Il n’y a pas de signatures
signed_with_trusted_certificate: Signé avec un certificat de confiance signed_with_trusted_certificate: Signé avec un certificat de confiance
signed_with_external_certificate: Signé avec un certificat externe signed_with_external_certificate: Signé avec un certificat externe
signature_valid: Signature valide
signature_verification_failed: Échec de la vérification de la signature
contains_unsigned_changes_after_the_last_signature: Contient des modifications non signées après la dernière signature
setup_2fa_to_continue: Configurez la 2FA pour continuer. setup_2fa_to_continue: Configurez la 2FA pour continuer.
create_a_new_account: Créer un nouveau compte. create_a_new_account: Créer un nouveau compte.
you_are_already_signed_in: Vous êtes déjà connecté. you_are_already_signed_in: Vous êtes déjà connecté.
@@ -4288,6 +4382,7 @@ fr: &fr
invite: Invitation invite: Invitation
link: Lien link: Lien
mcp: MCP mcp: MCP
self: Signer vous-même
submission_event_names: submission_event_names:
send_email_to_html: "<b>E‑mail envoyé</b> à %{submitter_name}" send_email_to_html: "<b>E‑mail envoyé</b> à %{submitter_name}"
bounce_email_html: "<b>E‑mail rejeté</b> %{submitter_name}" bounce_email_html: "<b>E‑mail rejeté</b> %{submitter_name}"
@@ -4401,9 +4496,29 @@ fr: &fr
submission_submitters: Liste des signataires submission_submitters: Liste des signataires
submission_link: Lien de la soumission submission_link: Lien de la soumission
documents_link: Lien des documents documents_link: Lien des documents
date:
abbr_month_names:
-
- janv.
- févr.
- mars
- avr.
- mai
- juin
- juil.
- août
- sept.
- oct.
- nov.
- déc.
formats:
short: "%-d %b"
long: "%-d %B %Y"
time: time:
formats: formats:
detailed: "%A %d %B %Y %Hh%Mm%Ss" short: "%-d %b, %H:%M"
long: "%-d %B %Y à %H:%M"
detailed: "%-d %B %Y à %H:%M:%S"
pt: &pt pt: &pt
knowledge_based_authentication: Autenticação baseada em conhecimento knowledge_based_authentication: Autenticação baseada em conhecimento
@@ -4435,8 +4550,11 @@ pt: &pt
paypal_integration: Integração com PayPal paypal_integration: Integração com PayPal
paypal_account_has_been_connected: Conta PayPal foi conectada. paypal_account_has_been_connected: Conta PayPal foi conectada.
re_connect_paypal: Reconectar PayPal re_connect_paypal: Reconectar PayPal
failed_to_connect_paypal: Falha ao conectar PayPal. Tente novamente. failed_to_connect_paypal_please_try_again: Falha ao conectar PayPal. Tente novamente.
paypal_connect_not_completed: A conexão com PayPal não foi concluída. paypal_connection_was_not_completed: A conexão com PayPal não foi concluída.
disconnecting_your_paypal_account_will_prevent_you_from_offering_paypal_services_and_products_on_your_website_do_you_wish_to_continue: Desconectar sua conta PayPal impedirá que você ofereça serviços e produtos PayPal no seu site. Deseja continuar?
attention_you_currently_cannot_receive_payments_due_to_restriction_on_your_paypal_account_please_reach_out_to_paypal_customer_support_or_connect_to_paypal_com_for_more_information: 'Atenção: No momento você não pode receber pagamentos devido a uma restrição na sua conta PayPal. Entre em contato com o Suporte ao Cliente PayPal ou acesse https://www.paypal.com para mais informações.'
attention_please_confirm_your_email_address_on_paypal_com_in_order_to_receive_payments_you_currently_cannot_receive_payments: 'Atenção: Por favor, confirme seu endereço de e-mail em https://www.paypal.com/businessprofile/settings para poder receber pagamentos! No momento você não pode receber pagamentos.'
payment_receipt: Recibo de pagamento payment_receipt: Recibo de pagamento
amount_paid: Valor pago amount_paid: Valor pago
transaction: Transação transaction: Transação
@@ -4613,6 +4731,7 @@ pt: &pt
profile_details: Detalhes do perfil profile_details: Detalhes do perfil
sign_up_with_google: Inscrever-se com Google sign_up_with_google: Inscrever-se com Google
sign_up_with_microsoft: Inscrever-se com Microsoft sign_up_with_microsoft: Inscrever-se com Microsoft
redirecting_to_provider: Redirecionando para %{provider}...
by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.com/terms">Termos de Serviço</a>.' by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.com/terms">Termos de Serviço</a>.'
enter_email_to_continue: Insira o e-mail para continuar enter_email_to_continue: Insira o e-mail para continuar
account: Conta account: Conta
@@ -4740,6 +4859,13 @@ pt: &pt
completed_form_message: Mensagem de formulário concluído completed_form_message: Mensagem de formulário concluído
completed_form_redirect_button: Botão de redirecionamento do formulário concluído completed_form_redirect_button: Botão de redirecionamento do formulário concluído
documents_copy_email: E-mail com cópia dos documentos documents_copy_email: E-mail com cópia dos documentos
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-mail de solicitação de assinatura enviado aos destinatários com um link para revisar e assinar.
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-mail somente de leitura enviado aos destinatários que precisam apenas revisar os documentos.
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Lembra os destinatários que ainda não concluíram a sua parte de assinar, de acordo com o agendamento de lembretes.
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS de solicitação de assinatura enviado aos destinatários com um link para revisar e assinar.
phone_2fa_verification_text_message: Mensagem de texto de verificação 2FA por telefone.
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: E-mail de notificação enviado ao usuário remetente e aos endereços BCC assim que a submissão for concluída por todas as partes.
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-mail com a cópia dos documentos concluídos enviado aos destinatários na conclusão, com os seus documentos.
form_custom_css: CSS personalizado do formulário form_custom_css: CSS personalizado do formulário
subject: Assunto subject: Assunto
body: Corpo body: Corpo
@@ -4981,6 +5107,9 @@ pt: &pt
there_are_no_signatures: Não há assinaturas there_are_no_signatures: Não há assinaturas
signed_with_trusted_certificate: Assinado com certificado confiável signed_with_trusted_certificate: Assinado com certificado confiável
signed_with_external_certificate: Assinado com certificado externo signed_with_external_certificate: Assinado com certificado externo
signature_valid: Assinatura válida
signature_verification_failed: Falha na verificação da assinatura
contains_unsigned_changes_after_the_last_signature: Contém alterações não assinadas após a última assinatura
setup_2fa_to_continue: Configure 2FA para continuar. setup_2fa_to_continue: Configure 2FA para continuar.
create_a_new_account: Criar uma nova conta. create_a_new_account: Criar uma nova conta.
you_are_already_signed_in: Você já está conectado. you_are_already_signed_in: Você já está conectado.
@@ -5385,6 +5514,7 @@ pt: &pt
invite: Convite invite: Convite
link: Link link: Link
mcp: MCP mcp: MCP
self: Assine você mesmo
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email enviado</b> para %{submitter_name}' send_email_to_html: '<b>Email enviado</b> para %{submitter_name}'
bounce_email_html: '<b>Email não entregue</b> %{submitter_name}' bounce_email_html: '<b>Email não entregue</b> %{submitter_name}'
@@ -5498,9 +5628,29 @@ pt: &pt
submission_submitters: Lista de signatários submission_submitters: Lista de signatários
submission_link: Link da submissão submission_link: Link da submissão
documents_link: Link dos documentos documents_link: Link dos documentos
date:
abbr_month_names:
-
- jan.
- fev.
- mar.
- abr.
- mai.
- jun.
- jul.
- ago.
- set.
- out.
- nov.
- dez.
formats:
short: "%-d de %b"
long: "%-d de %B de %Y"
time: time:
formats: formats:
detailed: "%A, %d de %B de %Y, %H:%M:%Sh" short: "%-d de %b, %H:%M"
long: "%-d de %B de %Y às %H:%M"
detailed: "%-d de %B de %Y às %H:%M:%S"
de: &de de: &de
knowledge_based_authentication: Wissensbasierte Authentifizierung knowledge_based_authentication: Wissensbasierte Authentifizierung
@@ -5542,8 +5692,11 @@ de: &de
paypal_integration: PayPal-Integration paypal_integration: PayPal-Integration
paypal_account_has_been_connected: PayPal-Konto wurde verbunden. paypal_account_has_been_connected: PayPal-Konto wurde verbunden.
re_connect_paypal: PayPal erneut verbinden re_connect_paypal: PayPal erneut verbinden
failed_to_connect_paypal: PayPal konnte nicht verbunden werden. Bitte erneut versuchen. failed_to_connect_paypal_please_try_again: PayPal konnte nicht verbunden werden. Bitte erneut versuchen.
paypal_connect_not_completed: Die PayPal-Verbindung wurde nicht abgeschlossen. paypal_connection_was_not_completed: Die PayPal-Verbindung wurde nicht abgeschlossen.
disconnecting_your_paypal_account_will_prevent_you_from_offering_paypal_services_and_products_on_your_website_do_you_wish_to_continue: Das Trennen Ihres PayPal-Kontos verhindert, dass Sie PayPal-Dienste und -Produkte auf Ihrer Website anbieten können. Möchten Sie fortfahren?
attention_you_currently_cannot_receive_payments_due_to_restriction_on_your_paypal_account_please_reach_out_to_paypal_customer_support_or_connect_to_paypal_com_for_more_information: 'Achtung: Sie können derzeit aufgrund einer Einschränkung Ihres PayPal-Kontos keine Zahlungen empfangen. Bitte wenden Sie sich an den PayPal-Kundenservice oder besuchen Sie https://www.paypal.com für weitere Informationen.'
attention_please_confirm_your_email_address_on_paypal_com_in_order_to_receive_payments_you_currently_cannot_receive_payments: 'Achtung: Bitte bestätigen Sie Ihre E-Mail-Adresse auf https://www.paypal.com/businessprofile/settings, um Zahlungen empfangen zu können! Sie können derzeit keine Zahlungen empfangen.'
payment_receipt: Zahlungsbeleg payment_receipt: Zahlungsbeleg
amount_paid: Gezahlter Betrag amount_paid: Gezahlter Betrag
transaction: Transaktion transaction: Transaktion
@@ -5710,6 +5863,7 @@ de: &de
profile_details: Profildetails profile_details: Profildetails
sign_up_with_google: Mit Google registrieren sign_up_with_google: Mit Google registrieren
sign_up_with_microsoft: Mit Microsoft registrieren sign_up_with_microsoft: Mit Microsoft registrieren
redirecting_to_provider: Weiterleitung zu %{provider}...
by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmen Sie unseren <a target="_blank" href="https://www.docuseal.com/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.com/terms">Nutzungsbedingungen</a> zu.' by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmen Sie unseren <a target="_blank" href="https://www.docuseal.com/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.com/terms">Nutzungsbedingungen</a> zu.'
enter_email_to_continue: E-Mail eingeben, um fortzufahren enter_email_to_continue: E-Mail eingeben, um fortzufahren
account: Konto account: Konto
@@ -5837,6 +5991,13 @@ de: &de
completed_form_message: Nachricht zum abgeschlossenen Formular completed_form_message: Nachricht zum abgeschlossenen Formular
completed_form_redirect_button: Weiterleitungsschaltfläche nach Formularabschluss completed_form_redirect_button: Weiterleitungsschaltfläche nach Formularabschluss
documents_copy_email: E-Mail mit Dokumentenkopie documents_copy_email: E-Mail mit Dokumentenkopie
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-Mail mit der Signaturanfrage, die mit einem Link zum Prüfen und Signieren an die Empfänger gesendet wird.
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-Mail nur zur Ansicht, die an Empfänger gesendet wird, die die Dokumente nur prüfen müssen.
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Erinnert Empfänger, die ihren Teil noch nicht abgeschlossen haben, an das Signieren, gemäß dem Erinnerungszeitplan.
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: SMS mit der Signaturanfrage, die mit einem Link zum Prüfen und Signieren an die Empfänger gesendet wird.
phone_2fa_verification_text_message: Textnachricht zur 2FA-Verifizierung per Telefon.
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Benachrichtigungs-E-Mail, die an den absendenden Benutzer und die BCC-Adressen gesendet wird, sobald die Einreichung von allen Parteien abgeschlossen wurde.
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-Mail mit der Kopie der abgeschlossenen Dokumente, die nach Abschluss an die Empfänger gesendet wird.
form_custom_css: Benutzerdefiniertes CSS für Formular form_custom_css: Benutzerdefiniertes CSS für Formular
subject: Betreff subject: Betreff
body: Inhalt body: Inhalt
@@ -6078,6 +6239,9 @@ de: &de
there_are_no_signatures: Es gibt keine Unterschriften there_are_no_signatures: Es gibt keine Unterschriften
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
signed_with_external_certificate: Signiert mit externem Zertifikat signed_with_external_certificate: Signiert mit externem Zertifikat
signature_valid: Signatur gültig
signature_verification_failed: Signaturprüfung fehlgeschlagen
contains_unsigned_changes_after_the_last_signature: Enthält nicht signierte Änderungen nach der letzten Signatur
setup_2fa_to_continue: Richten Sie 2FA ein, um fortzufahren. setup_2fa_to_continue: Richten Sie 2FA ein, um fortzufahren.
create_a_new_account: Neues Konto erstellen. create_a_new_account: Neues Konto erstellen.
you_are_already_signed_in: Sie sind bereits angemeldet. you_are_already_signed_in: Sie sind bereits angemeldet.
@@ -6482,6 +6646,7 @@ de: &de
invite: Einladung invite: Einladung
link: Link link: Link
mcp: MCP mcp: MCP
self: Selbst unterschreiben
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}' send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
bounce_email_html: '<b>E-Mail unzustellbar</b> %{submitter_name}' bounce_email_html: '<b>E-Mail unzustellbar</b> %{submitter_name}'
@@ -6595,11 +6760,42 @@ de: &de
submission_submitters: Liste der Unterzeichner submission_submitters: Liste der Unterzeichner
submission_link: Link der Einreichung submission_link: Link der Einreichung
documents_link: Link der Dokumente documents_link: Link der Dokumente
date:
abbr_month_names:
-
- Jan.
- Feb.
- März
- Apr.
- Mai
- Juni
- Juli
- Aug.
- Sept.
- Okt.
- Nov.
- Dez.
formats:
default: "%d.%m.%Y"
short: "%-d. %b"
long: "%-d. %B %Y"
time: time:
formats: formats:
detailed: "%A, %d. %B %Y, %H:%M:%S Uhr" short: "%-d. %b, %H:%M"
long: "%-d. %B %Y um %H:%M"
detailed: "%-d. %B %Y um %H:%M:%S"
pl: pl:
date:
formats:
default: "%d.%m.%Y"
short: "%-d %b"
long: "%-d %B %Y"
time:
formats:
short: "%-d %b, %H:%M"
long: "%-d %B %Y %H:%M"
detailed: "%-d %B %Y %H:%M:%S"
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Nadawca zażądał uwierzytelnienia dwuskładnikowego poprzez jednorazowe hasło wysłane na Twój numer telefonu <b>%{phone}</b>. the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Nadawca zażądał uwierzytelnienia dwuskładnikowego poprzez jednorazowe hasło wysłane na Twój numer telefonu <b>%{phone}</b>.
send_verification_code: Wyślij kod weryfikacyjny send_verification_code: Wyślij kod weryfikacyjny
@@ -6666,6 +6862,7 @@ pl:
profile_details: Szczegóły profilu profile_details: Szczegóły profilu
sign_up_with_google: Zarejestruj się przez Google sign_up_with_google: Zarejestruj się przez Google
sign_up_with_microsoft: Zarejestruj się przez Microsoft sign_up_with_microsoft: Zarejestruj się przez Microsoft
redirecting_to_provider: Przekierowywanie do %{provider}...
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.com/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.com/terms">Regulamin</a>.' by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.com/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.com/terms">Regulamin</a>.'
enter_email_to_continue: Wprowadź e-mail, aby kontynuować enter_email_to_continue: Wprowadź e-mail, aby kontynuować
the_code_has_been_sent_to_your_email: Kod został wysłany na Twój e-mail. the_code_has_been_sent_to_your_email: Kod został wysłany na Twój e-mail.
@@ -6701,6 +6898,44 @@ pl:
too_many_requests_try_again_later: Zbyt wiele żądań. Spróbuj ponownie później. too_many_requests_try_again_later: Zbyt wiele żądań. Spróbuj ponownie później.
uk: uk:
date:
month_names:
-
- січня
- лютого
- березня
- квітня
- травня
- червня
- липня
- серпня
- вересня
- жовтня
- листопада
- грудня
abbr_month_names:
-
- січ.
- лют.
- бер.
- квіт.
- трав.
- черв.
- лип.
- серп.
- вер.
- жовт.
- лист.
- груд.
formats:
default: "%d.%m.%Y"
short: "%-d %b"
long: "%-d %B %Y р."
time:
formats:
short: "%-d %b, %H:%M"
long: "%-d %B %Y р. о %H:%M"
detailed: "%-d %B %Y р. о %H:%M:%S"
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Відправник запитав двофакторну автентифікацію за допомогою одноразового пароля, надісланого на ваш номер телефону <b>%{phone}</b>. the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Відправник запитав двофакторну автентифікацію за допомогою одноразового пароля, надісланого на ваш номер телефону <b>%{phone}</b>.
send_verification_code: Надіслати код перевірки send_verification_code: Надіслати код перевірки
@@ -6767,6 +7002,7 @@ uk:
profile_details: Дані профілю profile_details: Дані профілю
sign_up_with_google: Зареєструватися через Google sign_up_with_google: Зареєструватися через Google
sign_up_with_microsoft: Зареєструватися через Microsoft sign_up_with_microsoft: Зареєструватися через Microsoft
redirecting_to_provider: Перенаправлення до %{provider}...
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.com/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.com/terms">Умовами надання послуг</a>.' by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.com/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.com/terms">Умовами надання послуг</a>.'
enter_email_to_continue: Введіть електронну пошту, щоб продовжити enter_email_to_continue: Введіть електронну пошту, щоб продовжити
the_code_has_been_sent_to_your_email: Код було надіслано на вашу електронну пошту. the_code_has_been_sent_to_your_email: Код було надіслано на вашу електронну пошту.
@@ -6802,6 +7038,44 @@ uk:
too_many_requests_try_again_later: Забагато запитів. Спробуйте пізніше. too_many_requests_try_again_later: Забагато запитів. Спробуйте пізніше.
cs: cs:
date:
month_names:
-
- ledna
- února
- března
- dubna
- května
- června
- července
- srpna
- září
- října
- listopadu
- prosince
abbr_month_names:
-
- led
- úno
- bře
- dub
- kvě
- čvn
- čvc
- srp
- zář
- říj
- lis
- pro
formats:
default: "%d. %m. %Y"
short: "%-d. %-m."
long: "%-d. %B %Y"
time:
formats:
short: "%-d. %-m. %-H:%M"
long: "%-d. %B %Y v %-H:%M"
detailed: "%-d. %B %Y v %-H:%M:%S"
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Odesílatel požádal o dvoufaktorové ověření pomocí jednorázového hesla zaslaného na vaše telefonní číslo <b>%{phone}</b>. the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Odesílatel požádal o dvoufaktorové ověření pomocí jednorázového hesla zaslaného na vaše telefonní číslo <b>%{phone}</b>.
send_verification_code: Odeslat ověřovací kód send_verification_code: Odeslat ověřovací kód
@@ -6868,6 +7142,7 @@ cs:
profile_details: Detaily profilu profile_details: Detaily profilu
sign_up_with_google: Zaregistrovat se pomocí Googlu sign_up_with_google: Zaregistrovat se pomocí Googlu
sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu
redirecting_to_provider: Přesměrování na %{provider}...
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.com/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.com/terms">Podmínkami služby</a>.' by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.com/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.com/terms">Podmínkami služby</a>.'
enter_email_to_continue: Zadejte e-mail pro pokračování enter_email_to_continue: Zadejte e-mail pro pokračování
the_code_has_been_sent_to_your_email: Kód byl odeslán na váš e-mail. the_code_has_been_sent_to_your_email: Kód byl odeslán na váš e-mail.
@@ -6903,6 +7178,30 @@ cs:
too_many_requests_try_again_later: Příliš mnoho požadavků. Zkuste to později. too_many_requests_try_again_later: Příliš mnoho požadavků. Zkuste to později.
he: he:
date:
abbr_month_names:
-
- ינו׳
- פבר׳
- מרץ
- אפר׳
- מאי
- יוני
- יולי
- אוג׳
- ספט׳
- אוק׳
- נוב׳
- דצמ׳
formats:
default: "%d.%m.%Y"
short: "%-d ב%b"
long: "%-d ב%B %Y"
time:
formats:
short: "%-d ב%b, %-H:%M"
long: "%-d ב%B %Y בשעה %-H:%M"
detailed: "%-d ב%B %Y בשעה %-H:%M:%S"
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: השולח ביקש אימות דו-שלבי באמצעות סיסמה חד פעמית שנשלחה למספר הטלפון שלך <b>%{phone}</b>. the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: השולח ביקש אימות דו-שלבי באמצעות סיסמה חד פעמית שנשלחה למספר הטלפון שלך <b>%{phone}</b>.
send_verification_code: שלח קוד אימות send_verification_code: שלח קוד אימות
@@ -6969,6 +7268,7 @@ he:
profile_details: פרטי הפרופיל profile_details: פרטי הפרופיל
sign_up_with_google: הירשם עם גוגל sign_up_with_google: הירשם עם גוגל
sign_up_with_microsoft: הירשם עם מיקרוסופט sign_up_with_microsoft: הירשם עם מיקרוסופט
redirecting_to_provider: מפנה אל %{provider}...
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.com/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.com/terms">תנאי השירות</a> שלנו.' by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.com/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.com/terms">תנאי השירות</a> שלנו.'
enter_email_to_continue: הכנס דוא"ל כדי להמשיך enter_email_to_continue: הכנס דוא"ל כדי להמשיך
the_code_has_been_sent_to_your_email: הקוד נשלח לדוא"ל שלך. the_code_has_been_sent_to_your_email: הקוד נשלח לדוא"ל שלך.
@@ -7043,8 +7343,11 @@ nl: &nl
paypal_integration: PayPal-integratie paypal_integration: PayPal-integratie
paypal_account_has_been_connected: PayPal-account is verbonden. paypal_account_has_been_connected: PayPal-account is verbonden.
re_connect_paypal: PayPal opnieuw verbinden re_connect_paypal: PayPal opnieuw verbinden
failed_to_connect_paypal: PayPal kon niet worden verbonden. Probeer het opnieuw. failed_to_connect_paypal_please_try_again: PayPal kon niet worden verbonden. Probeer het opnieuw.
paypal_connect_not_completed: De verbinding met PayPal is niet voltooid. paypal_connection_was_not_completed: De verbinding met PayPal is niet voltooid.
disconnecting_your_paypal_account_will_prevent_you_from_offering_paypal_services_and_products_on_your_website_do_you_wish_to_continue: Het loskoppelen van uw PayPal-account voorkomt dat u PayPal-diensten en -producten op uw website kunt aanbieden. Wilt u doorgaan?
attention_you_currently_cannot_receive_payments_due_to_restriction_on_your_paypal_account_please_reach_out_to_paypal_customer_support_or_connect_to_paypal_com_for_more_information: 'Let op: U kunt momenteel geen betalingen ontvangen vanwege een beperking op uw PayPal-account. Neem contact op met de PayPal-klantenservice of ga naar https://www.paypal.com voor meer informatie.'
attention_please_confirm_your_email_address_on_paypal_com_in_order_to_receive_payments_you_currently_cannot_receive_payments: 'Let op: Bevestig uw e-mailadres op https://www.paypal.com/businessprofile/settings om betalingen te kunnen ontvangen! U kunt momenteel geen betalingen ontvangen.'
payment_receipt: Betalingsbewijs payment_receipt: Betalingsbewijs
amount_paid: Betaald bedrag amount_paid: Betaald bedrag
transaction: Transactie transaction: Transactie
@@ -7212,6 +7515,7 @@ nl: &nl
profile_details: Profielgegevens profile_details: Profielgegevens
sign_up_with_google: Registreren met Google sign_up_with_google: Registreren met Google
sign_up_with_microsoft: Registreren met Microsoft sign_up_with_microsoft: Registreren met Microsoft
redirecting_to_provider: Doorverwijzen naar %{provider}...
by_creating_an_account_you_agree_to_our_html: Door een account aan te maken gaat u akkoord met ons <a target="_blank" href="https://www.docuseal.com/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.com/terms">Servicevoorwaarden</a>. by_creating_an_account_you_agree_to_our_html: Door een account aan te maken gaat u akkoord met ons <a target="_blank" href="https://www.docuseal.com/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.com/terms">Servicevoorwaarden</a>.
enter_email_to_continue: Voer e-mailadres in om door te gaan enter_email_to_continue: Voer e-mailadres in om door te gaan
account: Account account: Account
@@ -7339,6 +7643,13 @@ nl: &nl
completed_form_message: Bericht voltooid formulier completed_form_message: Bericht voltooid formulier
completed_form_redirect_button: Knop voor doorverwijzing na voltooid formulier completed_form_redirect_button: Knop voor doorverwijzing na voltooid formulier
documents_copy_email: E-mail met kopie van documenten documents_copy_email: E-mail met kopie van documenten
signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign: E-mail met het handtekeningverzoek die naar de ontvangers wordt verzonden met een link om te bekijken en te ondertekenen.
view_only_email_sent_to_recipients_that_need_to_only_review_the_documents: E-mail met alleen weergave die wordt verzonden naar ontvangers die de documenten alleen hoeven te bekijken.
reminds_recipients_who_havent_completed_their_part_yet_to_sign_following_the_reminders_schedule: Herinnert ontvangers die hun deel nog niet hebben voltooid eraan te ondertekenen, volgens het herinneringsschema.
signature_request_sms_sent_to_recipients_with_a_link_to_review_and_sign: Sms met het handtekeningverzoek die naar de ontvangers wordt verzonden met een link om te bekijken en te ondertekenen.
phone_2fa_verification_text_message: Sms-bericht voor 2FA-verificatie via telefoon.
notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties: Meldingse-mail die naar de verzendende gebruiker en de BCC-adressen wordt gestuurd zodra de indiening door alle partijen is voltooid.
completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents: E-mail met een kopie van de voltooide documenten die na voltooiing naar de ontvangers wordt verzonden.
form_custom_css: Aangepaste CSS voor formulier form_custom_css: Aangepaste CSS voor formulier
subject: Onderwerp subject: Onderwerp
body: Inhoud body: Inhoud
@@ -7580,6 +7891,9 @@ nl: &nl
there_are_no_signatures: Er zijn geen handtekeningen there_are_no_signatures: Er zijn geen handtekeningen
signed_with_trusted_certificate: Ondertekend met vertrouwd certificaat signed_with_trusted_certificate: Ondertekend met vertrouwd certificaat
signed_with_external_certificate: Ondertekend met extern certificaat signed_with_external_certificate: Ondertekend met extern certificaat
signature_valid: Handtekening geldig
signature_verification_failed: Verificatie van handtekening mislukt
contains_unsigned_changes_after_the_last_signature: Bevat niet-ondertekende wijzigingen na de laatste handtekening
setup_2fa_to_continue: Stel 2FA in om door te gaan. setup_2fa_to_continue: Stel 2FA in om door te gaan.
create_a_new_account: Maak een nieuw account aan. create_a_new_account: Maak een nieuw account aan.
you_are_already_signed_in: U bent al aangemeld. you_are_already_signed_in: U bent al aangemeld.
@@ -7980,6 +8294,7 @@ nl: &nl
invite: Uitnodiging invite: Uitnodiging
link: Link link: Link
mcp: MCP mcp: MCP
self: Zelf ondertekenen
submission_event_names: submission_event_names:
send_email_to_html: "<b>E-mail verzonden</b> naar %{submitter_name}" send_email_to_html: "<b>E-mail verzonden</b> naar %{submitter_name}"
bounce_email_html: "<b>E-mail geretourneerd</b> %{submitter_name}" bounce_email_html: "<b>E-mail geretourneerd</b> %{submitter_name}"
@@ -8093,11 +8408,29 @@ nl: &nl
submission_submitters: Lijst van ondertekenaars submission_submitters: Lijst van ondertekenaars
submission_link: Link van de inzending submission_link: Link van de inzending
documents_link: Link van de documenten documents_link: Link van de documenten
date:
formats:
short: "%-d %b"
long: "%-d %B %Y"
time: time:
formats: formats:
detailed: "%d %B %Y %H:%M:%S" short: "%-d %b, %H:%M"
long: "%-d %B %Y om %H:%M"
detailed: "%-d %B %Y om %H:%M:%S"
ar: ar:
date:
formats:
default: "%d‏/%m‏/%Y"
short: "%-d %b"
long: "%-d %B %Y"
time:
am: "ص"
pm: "م"
formats:
short: "%-d %b، %-I:%M %p"
long: "%-d %B %Y في %-I:%M %p"
detailed: "%-d %B %Y في %-I:%M:%S %p"
require_phone_2fa_to_open: "يتطلب المصادقة الثنائية عبر الهاتف للفتح" require_phone_2fa_to_open: "يتطلب المصادقة الثنائية عبر الهاتف للفتح"
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: "طلب المرسل المصادقة الثنائية عبر كلمة مرور لمرة واحدة مرسلة إلى رقم هاتفك <b>%{phone}</b>." the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: "طلب المرسل المصادقة الثنائية عبر كلمة مرور لمرة واحدة مرسلة إلى رقم هاتفك <b>%{phone}</b>."
send_verification_code: "إرسال رمز التحقق" send_verification_code: "إرسال رمز التحقق"
@@ -8164,6 +8497,7 @@ ar:
profile_details: تفاصيل الملف الشخصي profile_details: تفاصيل الملف الشخصي
sign_up_with_google: الاشتراك باستخدام جوجل sign_up_with_google: الاشتراك باستخدام جوجل
sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت
redirecting_to_provider: جارٍ إعادة التوجيه إلى %{provider}...
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.com/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.com/terms">شروط الخدمة</a> الخاصة بنا.' by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.com/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.com/terms">شروط الخدمة</a> الخاصة بنا.'
enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة
the_code_has_been_sent_to_your_email: تم إرسال الرمز إلى بريدك الإلكتروني. the_code_has_been_sent_to_your_email: تم إرسال الرمز إلى بريدك الإلكتروني.
@@ -8199,6 +8533,16 @@ ar:
too_many_requests_try_again_later: طلبات كثيرة جدًا. حاول مرة أخرى لاحقًا. too_many_requests_try_again_later: طلبات كثيرة جدًا. حاول مرة أخرى لاحقًا.
ko: ko:
date:
formats:
default: "%Y. %m. %d."
short: "%b %-d일"
long: "%Y년 %B %-d일"
time:
formats:
short: "%b %-d일 %p %-I:%M"
long: "%Y년 %B %-d일 %p %-I:%M"
detailed: "%Y년 %B %-d일 %p %-I:%M:%S"
require_phone_2fa_to_open: 열려면 휴대폰 2FA 요구 require_phone_2fa_to_open: 열려면 휴대폰 2FA 요구
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 발신자가 <b>%{phone}</b> 전화번호로 전송된 일회용 비밀번호를 통해 2단계 인증을 요청했습니다. the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 발신자가 <b>%{phone}</b> 전화번호로 전송된 일회용 비밀번호를 통해 2단계 인증을 요청했습니다.
send_verification_code: 인증 코드 보내기 send_verification_code: 인증 코드 보내기
@@ -8265,6 +8609,7 @@ ko:
profile_details: 프로필 세부 정보 profile_details: 프로필 세부 정보
sign_up_with_google: Google로 가입 sign_up_with_google: Google로 가입
sign_up_with_microsoft: Microsoft로 가입 sign_up_with_microsoft: Microsoft로 가입
redirecting_to_provider: "%{provider}(으)로 이동 중..."
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, <a target="_blank" href="https://www.docuseal.com/privacy">개인정보 처리방침</a> 및 <a target="_blank" href="https://www.docuseal.com/terms">서비스 약관</a>에 동의하게 됩니다.' by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, <a target="_blank" href="https://www.docuseal.com/privacy">개인정보 처리방침</a> 및 <a target="_blank" href="https://www.docuseal.com/terms">서비스 약관</a>에 동의하게 됩니다.'
enter_email_to_continue: 계속하려면 이메일을 입력하세요 enter_email_to_continue: 계속하려면 이메일을 입력하세요
the_code_has_been_sent_to_your_email: 코드가 이메일로 전송되었습니다. the_code_has_been_sent_to_your_email: 코드가 이메일로 전송되었습니다.
@@ -8300,6 +8645,16 @@ ko:
too_many_requests_try_again_later: 요청이 너무 많습니다. 나중에 다시 시도하세요. too_many_requests_try_again_later: 요청이 너무 많습니다. 나중에 다시 시도하세요.
ja: ja:
date:
formats:
default: "%Y/%m/%d"
short: "%-m月%-d日"
long: "%Y年%-m月%-d日"
time:
formats:
short: "%-m月%-d日 %-H:%M"
long: "%Y年%-m月%-d日 %-H:%M"
detailed: "%Y年%-m月%-d日 %-H:%M:%S"
require_phone_2fa_to_open: 電話による2段階認証が必要です require_phone_2fa_to_open: 電話による2段階認証が必要です
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 送信者は、<b>%{phone}</b> に送信されたワンタイムパスワードによる2段階認証を要求しました。 the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 送信者は、<b>%{phone}</b> に送信されたワンタイムパスワードによる2段階認証を要求しました。
send_verification_code: 認証コードを送信 send_verification_code: 認証コードを送信
@@ -8366,6 +8721,7 @@ ja:
profile_details: プロフィールの詳細 profile_details: プロフィールの詳細
sign_up_with_google: Googleで登録 sign_up_with_google: Googleで登録
sign_up_with_microsoft: Microsoftで登録 sign_up_with_microsoft: Microsoftで登録
redirecting_to_provider: "%{provider} にリダイレクトしています..."
by_creating_an_account_you_agree_to_our_html: '<a target="_blank" href="https://www.docuseal.com/privacy">プライバシーポリシー</a>および<a target="_blank" href="https://www.docuseal.com/terms">利用規約</a>に同意の上、アカウントを作成します。' by_creating_an_account_you_agree_to_our_html: '<a target="_blank" href="https://www.docuseal.com/privacy">プライバシーポリシー</a>および<a target="_blank" href="https://www.docuseal.com/terms">利用規約</a>に同意の上、アカウントを作成します。'
enter_email_to_continue: 続行するにはメールを入力してください enter_email_to_continue: 続行するにはメールを入力してください
the_code_has_been_sent_to_your_email: コードがあなたのメールに送信されました the_code_has_been_sent_to_your_email: コードがあなたのメールに送信されました
@@ -8402,21 +8758,33 @@ ja:
en-US: en-US:
<<: *en <<: *en
date:
formats:
default: "%m/%d/%Y"
time:
formats:
detailed: "%B %d, %Y %I:%M:%S %p"
en-GB: en-GB:
<<: *en <<: *en
date: date:
abbr_month_names:
-
- Jan
- Feb
- Mar
- Apr
- May
- Jun
- Jul
- Aug
- Sept
- Oct
- Nov
- Dec
formats: formats:
default: "%d/%m/%Y" default: "%d/%m/%Y"
short: "%-d %b"
long: "%-d %B %Y"
time: time:
formats: formats:
detailed: "%d %B, %Y %H:%M:%S" short: "%-d %b, %H:%M"
long: "%-d %B %Y at %H:%M"
detailed: "%-d %B %Y at %H:%M:%S"
es-ES: es-ES:
<<: *es <<: *es
+2 -1
View File
@@ -142,7 +142,8 @@ Rails.application.routes.draw do
get :completed get :completed
end end
resource :resubmit_form, controller: 'start_form', only: :update resource :resubmit_form, controller: 'start_form_resubmit', only: :update
resources :start_form_self, only: :update
resource :submit_form_email_2fa, only: %i[create update] resource :submit_form_email_2fa, only: %i[create update]
resources :start_form_email_2fa_send, only: :create resources :start_form_email_2fa_send, only: :create
-8
View File
@@ -1,8 +0,0 @@
development:
secret_key_base: 5e626815c7ef00cc5d7b46ff3099160f25e5271b00ced3b8eba1d5045eb0db0ddb925c85b2fea6b2acb4cbdcb779ba9c5a31600f31e6ee8944ada47a768f6a7f
test:
secret_key_base: 7d754998067691dc61859c6c3109966c44e1756a46fd2409f1d81403599605c103ff84b12102673b498bdbaba04d9949d8d7a2f7af47529ee6dc4dd19f0aef3e
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
@@ -6,7 +6,8 @@ class CreateLockEvents < ActiveRecord::Migration[8.0]
t.string :key, index: true, null: false t.string :key, index: true, null: false
t.string :event_name, null: false t.string :event_name, null: false
t.index %i[event_name key], unique: adapter_name != 'Mysql2', where: "event_name IN ('start', 'complete')" t.index %i[event_name key], unique: connection.supports_partial_index?,
where: "event_name IN ('start', 'complete')"
t.timestamps t.timestamps
end end
@@ -10,7 +10,7 @@ class AddIsFirstToCompletedSubmitters < ActiveRecord::Migration[8.0]
where: 'is_first = TRUE', where: 'is_first = TRUE',
name: 'index_completed_submitters_account_id_completed_at_is_first' name: 'index_completed_submitters_account_id_completed_at_is_first'
add_index :completed_submitters, :submission_id, unique: adapter_name != 'Mysql2', add_index :completed_submitters, :submission_id, unique: connection.supports_partial_index?,
where: 'is_first = TRUE' where: 'is_first = TRUE'
end end
end end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
class AddSubmissionsAccountTemplateStatistics < ActiveRecord::Migration[8.1]
def up
return unless adapter_name == 'PostgreSQL'
execute <<~SQL.squish
CREATE STATISTICS IF NOT EXISTS submissions_account_template_stats (dependencies, ndistinct)
ON account_id, template_id FROM submissions
SQL
end
def down
return unless adapter_name == 'PostgreSQL'
execute 'DROP STATISTICS IF EXISTS submissions_account_template_stats'
end
end
+1 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_07_07_055354) do ActiveRecord::Schema[8.1].define(version: 2026_08_19_091500) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "btree_gin" enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
+14 -20
View File
@@ -15,34 +15,28 @@ module PdfUtils
end end
def decrypt(data, password) 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_io = StringIO.new
decrypted_doc.write(decrypted_io, validate: false) Pdfium::Document.open_bytes(data, password) do |doc|
doc.save(decrypted_io, flags: Pdfium::FPDF_REMOVE_SECURITY)
end
decrypted_io.tap(&:rewind).read decrypted_io.tap(&:rewind).read
end end
def merge(files) def merge(io_files)
merged_pdf = HexaPDF::Document.new merged_content = StringIO.new
files.each do |file| Pdfium.with_instance do
pdf = HexaPDF::Document.new(io: file) Pdfium::Document.create do |merged_pdf|
pdf.pages.each { |page| merged_pdf.pages << merged_pdf.import(page) } io_files.each do |io|
Pdfium::Document.open_io(io) { |pdf| merged_pdf.import_pages(pdf) }
end
merged_pdf.save(merged_content)
end
end end
merged_content = StringIO.new merged_content.tap(&:rewind)
merged_pdf.validate(auto_correct: true)
merged_pdf.write(merged_content, validate: false)
merged_content.rewind
merged_content
end end
end end
+893 -17
View File
File diff suppressed because it is too large. Load diff
+14 -6
View File
@@ -219,7 +219,7 @@ module Submissions
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[{ text: document.filename.to_s }] [{ text: TextUtils.maybe_rtl_reverse(document.filename.to_s) }]
), ),
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
@@ -272,7 +272,9 @@ module Submissions
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
submission.template_submitters.size > 1 && { text: "#{item['name']}\n" }, submission.template_submitters.size > 1 && {
text: "#{TextUtils.maybe_rtl_reverse(item['name'].to_s)}\n"
},
submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] }, submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] },
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" }, submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
submitter.phone && { text: "#{submitter.phone}\n" } submitter.phone && { text: "#{submitter.phone}\n" }
@@ -425,7 +427,7 @@ module Submissions
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options) r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
end end
{ link:, text: "#{attachment.filename}\n", style: :link } { link:, text: "#{TextUtils.maybe_rtl_reverse(attachment.filename.to_s)}\n", style: :link }
end, end,
padding: [0, 0, 10, 0] padding: [0, 0, 10, 0]
) )
@@ -476,6 +478,8 @@ module Submissions
submitter.name || submitter.email || submitter.phone submitter.name || submitter.email || submitter.phone
end end
submitter_name = TextUtils.maybe_rtl_reverse(submitter_name.to_s)
text = text =
if event.event_type == 'complete_verification' if event.event_type == 'complete_verification'
I18n.t('submission_event_names.complete_verification_by_html', provider: event.data['method'], I18n.t('submission_event_names.complete_verification_by_html', provider: event.data['method'],
@@ -483,18 +487,22 @@ module Submissions
elsif event.event_type == 'invite_party' && elsif event.event_type == 'invite_party' &&
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) (name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
invited_submitter_name = [invited_submitter.name || invited_submitter.email || invited_submitter.phone, invited_submitter_name = TextUtils.maybe_rtl_reverse(
name].join(' ') [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' ')
)
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:, I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
submitter_name:) submitter_name:)
elsif with_audit_sender && (event.event_type == 'send_email' || event.event_type == 'send_sms') elsif with_audit_sender && (event.event_type == 'send_email' || event.event_type == 'send_sms')
created_by_name = TextUtils.maybe_rtl_reverse(submission.created_by_user.full_name)
[ [
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:), I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:),
"<b>#{I18n.t(:from)}</b> #{submission.created_by_user.full_name} #{submission.created_by_user.email}" "<b>#{I18n.t(:from)}</b> #{created_by_name} #{submission.created_by_user.email}"
].join("\n") ].join("\n")
elsif event.event_type == 'delegate_form' elsif event.event_type == 'delegate_form'
from = event.data['old_email'].presence || from = event.data['old_email'].presence ||
versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone } versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }
from = TextUtils.maybe_rtl_reverse(from.to_s)
I18n.t('submission_event_names.delegate_form_by_html', from:, to: event.data['email']) I18n.t('submission_event_names.delegate_form_by_html', from:, to: event.data['email'])
elsif event.event_type.include?('send_') elsif event.event_type.include?('send_')
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:) I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
@@ -346,8 +346,11 @@ module Submissions
(with_signature_id_completed_at ? submitter.completed_at : nil) || attachment.created_at (with_signature_id_completed_at ? submitter.completed_at : nil) || attachment.created_at
if with_signature_id_reason || field.dig('preferences', 'reasons').present? if with_signature_id_reason || field.dig('preferences', 'reasons').present?
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \ reason_line =
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \ "#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}"
"#{TextUtils.maybe_rtl_reverse(reason_line)}\n" \
"#{I18n.l(signature_timestamp.in_time_zone(timezone), format: time_format)} " \ "#{I18n.l(signature_timestamp.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, signature_timestamp)}" "#{TimeUtils.timezone_abbr(timezone, signature_timestamp)}"
else else
@@ -509,7 +512,9 @@ module Submissions
cv.image(PdfIcons.paperclip_io, at: [0, 0], width: box.content_width) cv.image(PdfIcons.paperclip_io, at: [0, 0], width: box.content_width)
end end
acc << HexaPDF::Layout::TextFragment.create("#{attachment.filename}\n", font:, font_size:) acc << HexaPDF::Layout::TextFragment.create(
"#{TextUtils.maybe_rtl_reverse(attachment.filename.to_s)}\n", font:, font_size:
)
end end
lines = layouter.fit(items, area['w'] * width, height).lines lines = layouter.fit(items, area['w'] * width, height).lines
+2 -1
View File
@@ -93,7 +93,8 @@ module Submitters
def plain_search(submitters, keyword) def plain_search(submitters, keyword)
return submitters if keyword.blank? return submitters if keyword.blank?
term = "%#{keyword.downcase}%" sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
term = "%#{sanitized}%"
arel_table = Submitter.arel_table arel_table = Submitter.arel_table
+145
View File
@@ -0,0 +1,145 @@
# frozen_string_literal: true
module Submitters
module StartForm
COOKIES_TTL = 12.hours
START_FORM_COOKIES_TTL = 7.days
RESUBMIT_TTL = 3.days
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
NotSaved = Class.new(StandardError)
module_function
def can_resubmit?(submitter)
submitter.completed_at? && submitter.completed_at > RESUBMIT_TTL.ago &&
%w[api embed mcp].exclude?(submitter.submission.source) &&
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
end
def can_reopen?(template, submitter, request:, current_user:)
return false if submitter.submission&.completed_at? && submitter.viewer?
return true if request.cookie_jar.encrypted[:start_form_slug] == submitter.slug
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
return false if submitter.email.blank?
return true if submitter.completed_at?
return false if submitter.submission.source_embed?
return true if template.preferences['shared_link_2fa'] == true
return false if submitter.submission.source_link?
return true if current_user && submitter.email == current_user.email &&
current_user.account_id == submitter.account_id
Docuseal.multitenant? || Accounts.can_send_emails?(template.account)
end
def assign_start_form_cookie(submitter, request)
request.cookie_jar.encrypted[:start_form_slug] =
{ value: submitter.slug, expires: START_FORM_COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
def find_or_initialize_submitter(template, submitter_params, exclude_completed:, request:, current_user: nil)
required_fields = template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
find_params = required_params.except('name')
submitter =
if find_params.compact_blank.blank?
Submitter.new
else
build_submitters_scope(template, exclude_completed:).find_or_initialize_by(find_params)
end
if submitter.persisted? && !can_reopen?(template, submitter, request:, current_user:)
submitter = Submitter.new(find_params)
end
submitter.name = required_params['name'] if submitter.new_record?
submitter
end
def build_submitters_scope(template, exclude_completed: false, source: nil)
submissions = template.submissions.non_expired.active
submissions = submissions.where(source:) if source
Submitter
.where(submission: submissions)
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.then { |rel| exclude_completed ? rel.where(completed_at: nil) : rel }
end
def assign_submission_attributes(submitter, template, ip:, user_agent:, source: :link, resubmit_submitter: nil)
submitter.assign_attributes(
uuid: first_submitter_uuid(template),
ip:,
ua: user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: resubmit_submitter&.metadata.presence || {}
)
submitter.assign_attributes(resubmit_submitter.slice(:name, :email, :phone)) if resubmit_submitter
if submitter.values.present?
resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end
end
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
expire_at: Templates.build_default_expire_at(template),
submitters: [submitter],
source:)
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
submitter.account_id = submitter.submission.account_id
submitter
end
def first_submitter_uuid(template)
(Templates.filter_undefined_submitters(template.submitters).first || template.submitters.first)['uuid']
end
def verify_2fa_and_save_submitter(submitter, request, is_new_record:)
is_otp_verified = Submitters.verify_link_otp!(request.params[:one_time_code], submitter)
return false if !is_otp_verified && request.cookie_jar.encrypted[:email_2fa_slug] != submitter.slug
raise NotSaved unless submitter.save
enqueue_new_submitter_jobs(submitter) if is_new_record
assign_start_form_cookie(submitter, request)
if is_otp_verified
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
request.cookie_jar.encrypted[:email_2fa_slug] =
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
true
end
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
SearchEntries.enqueue_reindex(submitter)
expire_at = submitter.submission.expire_at
return unless expire_at
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id,
'expire_at' => expire_at.to_i)
end
end
end
+1 -1
View File
@@ -114,7 +114,7 @@ module Submitters
end end
reason_field = submitter.submission.template_fields.find do |e| reason_field = submitter.submission.template_fields.find do |e|
e['uuid'] == reason_field_uuid && e['submitter_uuid'] == submitter.uuid e['uuid'] == reason_field_uuid
end end
if reason_field if reason_field
+53
View File
@@ -0,0 +1,53 @@
# frozen_string_literal: true
module Templates
module BuildPdfiumAnnotations
URI_PREFIXES = %w[https:// http://].freeze
LINKS_LIMIT = 250
module_function
def call(doc)
annotations = []
doc.page_count.times do |page_index|
break if annotations.size >= LINKS_LIMIT
next if doc.annot_count(page_index).zero?
page = doc.get_page(page_index)
geometry = { box: page.box, rotation: page.rotation }
page.annotations.each do |annotation|
next unless annotation.link? && annotation.rect?
url = annotation.link.url
next if url.blank? || URI_PREFIXES.none? { |prefix| url.start_with?(prefix) }
annotations << build_external_link_hash(url, annotation, geometry).merge('page' => page_index)
end
end
annotations
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
raise if Rails.env.development?
[]
end
def build_external_link_hash(url, area, geometry)
x, y, w, h, page_width, page_height = Pdfium.transform_rect(area.bounds, **geometry)
{
'type' => 'external_link',
'value' => url,
'x' => x / page_width,
'y' => y / page_height,
'w' => w / page_width,
'h' => h / page_height
}
end
end
end
+56 -13
View File
@@ -39,6 +39,8 @@ module Templates
end end
def handle_pdf_or_image(template, file, document_data = nil, params = {}, extract_fields: false, metadata: {}) def handle_pdf_or_image(template, file, document_data = nil, params = {}, extract_fields: false, metadata: {})
return handle_pdf_or_image_v2(template, file, document_data, params, extract_fields:, metadata:) if v2?
document_data ||= file.read document_data ||= file.read
if file.content_type == PDF_CONTENT_TYPE if file.content_type == PDF_CONTENT_TYPE
@@ -48,6 +50,55 @@ module Templates
document_data.size < ANNOTATIONS_SIZE_LIMIT ? Templates::BuildAnnotations.call(document_data) : [] document_data.size < ANNOTATIONS_SIZE_LIMIT ? Templates::BuildAnnotations.call(document_data) : []
end end
document = create_document(template, file, document_data, metadata, annotations)
Templates::ProcessDocument.call(document, document_data, extract_fields:)
end
def handle_pdf_or_image_v2(template, file, document_data = nil, params = {}, extract_fields: false, metadata: {})
document_data ||= file.read
unless file.content_type == PDF_CONTENT_TYPE
document = create_document(template, file, document_data, metadata)
return Templates::ProcessDocument.call(document, document_data, extract_fields:)
end
doc = Pdfium::Document.open_bytes(document_data, params[:password])
document_data = decrypt_document(doc) if doc.encrypted?
annotations =
document_data.size < ANNOTATIONS_SIZE_LIMIT ? Templates::BuildPdfiumAnnotations.call(doc) : []
document = create_document(template, file, document_data, metadata, annotations)
Templates::ProcessDocument.call(document, document_data, extract_fields:, doc:)
rescue Pdfium::PasswordError
raise PdfEncrypted
ensure
doc&.close
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 Pdfium::PasswordError
raise PdfEncrypted
end
def decrypt_document(doc)
io = StringIO.new
doc.save(io, flags: Pdfium::FPDF_REMOVE_SECURITY)
io.tap(&:rewind).read
end
def create_document(template, file, document_data, metadata, annotations = nil)
sha256 = Base64.urlsafe_encode64(Digest::SHA256.digest(document_data)) sha256 = Base64.urlsafe_encode64(Digest::SHA256.digest(document_data))
blob = ActiveStorage::Blob.create_and_upload!( blob = ActiveStorage::Blob.create_and_upload!(
@@ -62,19 +113,7 @@ module Templates
content_type: file.content_type content_type: file.content_type
) )
document = template.documents.create!(blob:) template.documents.create!(blob:)
Templates::ProcessDocument.call(document, document_data, extract_fields:)
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 end
def extract_zip_files(files) def extract_zip_files(files)
@@ -124,5 +163,9 @@ module Templates
raise InvalidFileType, "#{file.content_type}/#{dynamic}" raise InvalidFileType, "#{file.content_type}/#{dynamic}"
end end
def v2?
true
end
end end
end end
+13 -9
View File
@@ -173,20 +173,20 @@ module Templates
**attrs, **attrs,
type: 'select', type: 'select',
options: build_options(field[:Opt], 'select'), options: build_options(field[:Opt], 'select'),
default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value.presence
} }
elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present? elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
{ {
**attrs, **attrs,
type: 'multiple', type: 'multiple',
options: build_options(field[:Opt], 'multiple'), options: build_options(field[:Opt], 'multiple'),
default_value: field.field_value default_value: field.field_value.presence
} }
elsif field.field_type == :Tx && field.concrete_field_type == :comb_text_field elsif field.field_type == :Tx && field.concrete_field_type == :comb_text_field
{ {
**attrs, **attrs,
type: 'cells', type: 'cells',
default_value: field.field_value default_value: field.field_value.presence
} }
elsif field.field_type == :Tx elsif field.field_type == :Tx
if field[:AA] && ((field[:AA][:F] && field[:AA][:F][:JS].include?('AFDate_')) || if field[:AA] && ((field[:AA][:F] && field[:AA][:F][:JS].include?('AFDate_')) ||
@@ -199,13 +199,13 @@ module Templates
{ {
**attrs, **attrs,
type: 'date', type: 'date',
default_value: field.field_value default_value: field.field_value.presence
} }
else else
{ {
**attrs, **attrs,
type: 'text', type: 'text',
default_value: field.field_value default_value: field.field_value.presence
} }
end end
elsif field.field_type == :Sig elsif field.field_type == :Sig
@@ -234,7 +234,7 @@ module Templates
{ {
uuid: SecureRandom.uuid, uuid: SecureRandom.uuid,
value: is_option_number || is_skip_single_value ? '' : option value: is_option_number || is_skip_single_value ? '' : option.presence
} }
end end
end end
@@ -251,7 +251,9 @@ module Templates
fields_index[annot.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, annot) fields_index[annot.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, annot)
elsif annot.key?(:Parent) elsif annot.key?(:Parent)
field = annot[:Parent] field = annot[:Parent]
field = field[:Parent] while field[:Parent] seen = Set.new.compare_by_identity
field = field[:Parent] while field[:Parent] && seen.add?(field.value)
annots_index[field.hash] ||= page annots_index[field.hash] ||= page
fields_index[field.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, field) fields_index[field.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, field)
@@ -262,7 +264,7 @@ module Templates
[process_fields_array(pdf, fields_index.values), annots_index] [process_fields_array(pdf, fields_index.values), annots_index]
end end
def process_fields_array(pdf, array, acc = []) def process_fields_array(pdf, array, acc = [], seen = Set.new.compare_by_identity)
array.each_with_index do |field, index| array.each_with_index do |field, index|
next if field.nil? next if field.nil?
@@ -270,10 +272,12 @@ module Templates
array[index] = field = HexaPDF::Type::AcroForm::Field.wrap(pdf, field) array[index] = field = HexaPDF::Type::AcroForm::Field.wrap(pdf, field)
end end
next unless seen.add?(field.value)
if field.terminal_field? if field.terminal_field?
acc << field acc << field
else else
process_fields_array(pdf, field[:Kids], acc) process_fields_array(pdf, field[:Kids], acc, seen)
end end
end end
+230
View File
@@ -0,0 +1,230 @@
# frozen_string_literal: true
module Templates
module FindPdfiumAcroFields
DATE_JS_PREFIX = 'AFDate_'
SKIP_FIELD_TYPES = %i[unknown pushbutton].freeze
TEXT_OPERATOR_REGEXP = /\bT[jJ]\b/
module_function
def call(attachment, doc, data)
return [] if !doc.form? && data.exclude?('/Form')
pages = {}
widgets = []
doc.page_count.times do |page_index|
next if doc.annot_count(page_index).zero?
page = doc.get_page(page_index)
pages[page_index] = { box: page.box, rotation: page.rotation }
page.annotations.each do |annotation|
next unless annotation.widget? && annotation.rect?
field = annotation.field
next if field.nil? || field.type.in?(SKIP_FIELD_TYPES)
widgets << annotation
end
end
group_widgets(widgets).filter_map { |field_widgets| build_field(field_widgets, pages, attachment) }
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
raise if Rails.env.development?
[]
end
def group_widgets(widgets)
widgets.group_by do |annotation|
field = annotation.field
if (field.control_count > 1 && field.own?) || field.detached? || (field.name.blank? && field.control_count <= 1)
[:widget, annotation.page_index, annotation.index]
else
[:field, field.type, field.name]
end
end.values
end
def build_field(widgets, pages, attachment)
widgets = widgets.sort_by.with_index { |annotation, index| [annotation.field.control_index, index] }
areas = widgets.filter_map { |annotation| build_area(annotation, pages[annotation.page_index], attachment) }
return if areas.blank?
field_properties = build_field_properties(widgets)
return if field_properties.blank?
return if field_properties[:default_value].present?
if field_properties[:type] == 'radio'
if areas.size != field_properties[:options].size
field_properties[:options] = build_options(Array.new(areas.size, ''))
end
areas.each_with_index do |area, index|
area[:option_uuid] = field_properties[:options][index][:uuid]
end
end
{
uuid: SecureRandom.uuid,
required: widgets.first.field.required?,
preferences: {},
areas:,
**field_properties
}
end
def build_area(annotation, page, attachment)
x, y, w, h, page_width, page_height = Pdfium.transform_rect(annotation.bounds, **page)
attrs = {
page: annotation.page_index,
x: x / page_width,
y: y / page_height,
w: w / page_width,
h: h / page_height,
attachment_uuid: attachment.uuid
}
return if attrs[:w].zero? || attrs[:h].zero?
field = annotation.field
attrs[:cell_w] = attrs[:w] / field.max_len if field.comb? && field.max_len.to_f.positive?
attrs
end
def build_field_properties(widgets)
field = widgets.first.field
field_name = field.name if field.name.match?(FindAcroFields::FIELD_NAME_REGEXP)
attrs = { name: field_name.to_s }
attrs[:description] = field.alternate_name if field.alternate_name.present? &&
field.alternate_name != field.name &&
!field.alternate_name.in?(FindAcroFields::SKIP_FIELD_DESCRIPTION)
case field.type
when :checkbox, :radio
build_button_properties(attrs, widgets)
when :combobox
build_select_properties(attrs, widgets.first)
when :text
build_text_properties(attrs, field)
when :signature
{
**attrs,
type: field.name.to_s.downcase.include?('initials') ? 'initials' : 'signature'
}
else
{}
end.compact
end
def build_button_properties(attrs, widgets)
field = widgets.first.field
options = widgets.find { |w| w.field.options.present? }&.field&.options.to_a
checked = widgets.find { |w| w.field.checked? }
export_values = widgets.filter_map { |w| w.field.export_value.presence }.uniq
if field.type == :radio && options.present?
{
**attrs,
type: 'radio',
options: build_options(options, 'radio'),
default_value: checked && options[checked.field.control_index]
}
elsif field.control_count > 1 && export_values.size > 1
{
**attrs,
type: 'radio',
options: build_options(export_values.map(&:to_sym), 'radio'),
default_value: checked&.field&.export_value.presence
}
else
{
**attrs,
type: 'checkbox',
default_value: checked.present?
}
end
end
def build_select_properties(attrs, annotation)
field = annotation.field
return {} if field.options.blank?
value = field.value.presence if renders_text?(annotation)
{
**attrs,
type: 'select',
options: build_options(field.options, 'select'),
default_value: value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP) ? nil : value
}
end
def renders_text?(annotation)
appearance = annotation.page.with_annotation(annotation.index, &:appearance)
appearance.to_s.match?(TEXT_OPERATOR_REGEXP)
end
def build_text_properties(attrs, field)
preferences = { align: FindAcroFields::FIELD_ALIGNMENT.fetch(field.quadding.to_i, 'left') }
attrs = { **attrs, preferences: }
if field.comb?
{ **attrs, type: 'cells', default_value: field.value.presence }
elsif date?(field)
format = [field.format_js, field.keystroke_js].compact
.filter_map { |js| js[FindAcroFields::DATE_FORMAT_REGEXP] }
.first
preferences[:format] = format.upcase if format
{ **attrs, type: 'date', default_value: field.value.presence }
else
{ **attrs, type: 'text', default_value: field.value.presence }
end
end
def date?(field)
field.format_js.to_s.include?(DATE_JS_PREFIX) || field.keystroke_js.to_s.include?(DATE_JS_PREFIX)
end
def build_options(values, type = nil)
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
values.filter_map do |option|
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
option = option[1] if option.is_a?(Array) && option.size == 2
if option.is_a?(String) || option.is_a?(Symbol)
option = option.to_s.encode('utf-8', invalid: :replace, undef: :replace, replace: '')
end
next if type == 'select' && option.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
{
uuid: SecureRandom.uuid,
value: is_option_number || is_skip_single_value ? '' : option.presence
}
end
end
end
end
+61 -20
View File
@@ -19,15 +19,19 @@ module Templates
module_function module_function
def call(attachment, data, extract_fields: false, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED) def call(attachment, data, extract_fields: false, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED, doc: nil)
if attachment.content_type == PDF_CONTENT_TYPE if attachment.content_type == PDF_CONTENT_TYPE
if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
pdf = HexaPDF::Document.new(io: StringIO.new(data)) if doc
fields = Templates::FindPdfiumAcroFields.call(attachment, doc, data)
else
pdf = HexaPDF::Document.new(io: StringIO.new(data))
fields = Templates::FindAcroFields.call(pdf, attachment, data) fields = Templates::FindAcroFields.call(pdf, attachment, data)
end
end end
generate_pdf_preview_images(attachment, data, pdf, max_pages:) generate_pdf_preview_images(attachment, data, pdf, max_pages:, doc:)
attachment.metadata['pdf']['fields'] = fields if fields attachment.metadata['pdf']['fields'] = fields if fields
elsif attachment.image? elsif attachment.image?
@@ -37,16 +41,24 @@ module Templates
attachment attachment
end end
def process(attachment, data, extract_fields: false) def process(attachment, data, extract_fields: false, doc: nil)
if attachment.content_type == PDF_CONTENT_TYPE && extract_fields && data.size < MAX_FLATTEN_FILE_SIZE if attachment.content_type == PDF_CONTENT_TYPE && extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
pdf = HexaPDF::Document.new(io: StringIO.new(data)) if doc
fields = Templates::FindPdfiumAcroFields.call(attachment, doc, data)
else
pdf = HexaPDF::Document.new(io: StringIO.new(data))
fields = Templates::FindAcroFields.call(pdf, attachment, data) fields = Templates::FindAcroFields.call(pdf, attachment, data)
end
end end
pdf ||= HexaPDF::Document.new(io: StringIO.new(data)) if doc
number_of_pages = doc.page_count
else
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
number_of_pages = pdf.pages.size number_of_pages = pdf.pages.size
end
attachment.metadata['pdf'] ||= {} attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['number_of_pages'] = number_of_pages attachment.metadata['pdf']['number_of_pages'] = number_of_pages
@@ -76,13 +88,17 @@ module Templates
) )
end end
def generate_pdf_preview_images(attachment, data, pdf = nil, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED) def generate_pdf_preview_images(attachment, data, pdf = nil, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED, doc: nil)
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
pdf ||= HexaPDF::Document.new(io: StringIO.new(data)) if doc
number_of_pages = pdf.pages.size number_of_pages = doc.page_count
else
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
number_of_pages = pdf.pages.size
data = maybe_flatten_form(data, pdf) data = maybe_flatten_form(data, pdf)
end
attachment.metadata['pdf'] ||= {} attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['number_of_pages'] = number_of_pages attachment.metadata['pdf']['number_of_pages'] = number_of_pages
@@ -93,17 +109,21 @@ module Templates
max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? max_pages : 1 max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? max_pages : 1
generate_document_preview_images(attachment, data, 0..[number_of_pages - 1, max_pages_to_process].min) generate_document_preview_images(attachment, data, 0..[number_of_pages - 1, max_pages_to_process].min, doc:)
end end
def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY) def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY, doc: nil)
doc = Pdfium::Document.open_bytes(data) flatten_pages = doc&.form?
pdfium_doc = doc || Pdfium::Document.open_bytes(data)
pool = Concurrent::FixedThreadPool.new(concurrency) pool = Concurrent::FixedThreadPool.new(concurrency)
promises = promises =
range.map do |page_number| range.map do |page_number|
doc_page = doc.get_page(page_number) doc_page = pdfium_doc.get_page(page_number)
hide_placeholder_widgets(doc_page, hide_empty: !flatten_pages) if doc
doc_page.flatten if flatten_pages
bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH) bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
@@ -126,10 +146,31 @@ module Templates
end end
end end
ensure ensure
doc&.close pdfium_doc&.close if doc.nil?
pool&.kill pool&.kill
end end
def hide_placeholder_widgets(page, hide_empty: true)
page.annotations.each do |annotation|
next unless annotation.widget?
page.with_annotation(annotation.index) do |handle|
next if handle.field_type != Pdfium::FPDF_FORMFIELD_COMBOBOX
value = handle.field_value.to_s
if value.blank?
next unless hide_empty
elsif handle.option_labels.blank? ||
!value.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
next
end
handle.hide!
end
end
end
def build_and_upload_blob(image, page_number, format = FORMAT) def build_and_upload_blob(image, page_number, format = FORMAT)
image = image.copy(interpretation: :srgb) image = image.copy(interpretation: :srgb)
@@ -192,8 +233,8 @@ module Templates
end end
end end
def generate_pdf_preview_from_file(attachment, file_path, page_number) def generate_pdf_preview_from_io(attachment, io, page_number)
doc = Pdfium::Document.open_file(file_path) doc = Pdfium::Document.open_io(io)
doc_page = doc.get_page(page_number) doc_page = doc.get_page(page_number)
+170
View File
@@ -0,0 +1,170 @@
# frozen_string_literal: true
module VerifyPdfSignature
COMMON_NAME = 'CN'
TIME_FORMAT = '%Y%m%d%H%M%S%z'
SignatureStruct = Struct.new(:messages, :reason, :signing_time, :common_name, :type)
MessageStruct = Struct.new(:text, :status)
module_function
def call(io, trusted_certs)
Pdfium::Document.open_io(io) do |document|
signatures = document.signatures.select { |e| e.byte_range.any?(&:positive?) && e.contents.present? }
next [] if signatures.blank?
has_unsigned_changes = unsigned_changes?(document, io)
signatures.map.with_index do |signature, index|
build_signature(signature, io, trusted_certs,
has_unsigned_changes && index == signatures.size - 1)
end
end
end
def build_signature(signature, io, trusted_certs, has_unsigned_changes)
pkcs7 = OpenSSL::PKCS7.new(signature.contents)
verified = verify_contents(pkcs7, signed_data(io, signature.byte_range), trusted_certs)
SignatureStruct.new(
messages: build_messages(pkcs7, verified, trusted_certs, has_unsigned_changes),
reason: signature.reason,
signing_time: signing_time(pkcs7, signature),
common_name: common_name(pkcs7),
type: signature.sub_filter
)
rescue OpenSSL::PKCS7::PKCS7Error
SignatureStruct.new(
messages: [MessageStruct.new(text: I18n.t('signature_verification_failed'), status: :error)],
reason: signature.reason,
signing_time: parse_time(signature.time),
type: signature.sub_filter
)
end
def build_messages(pkcs7, verified, trusted_certs, has_unsigned_changes)
messages =
if verified
[MessageStruct.new(text: I18n.t('signature_valid'), status: :success),
certificate_message(pkcs7, trusted_certs)]
else
[MessageStruct.new(text: I18n.t('signature_verification_failed'), status: :error)]
end
if has_unsigned_changes
messages << MessageStruct.new(text: I18n.t('contains_unsigned_changes_after_the_last_signature'),
status: :warning)
end
messages << MessageStruct.new(text: "Certificate chain: #{certificate_chain(pkcs7).join(' -> ')}")
end
def certificate_message(pkcs7, trusted_certs)
public_key = signer_certificate(pkcs7)&.public_key&.to_der
if trusted_certs.any? { |e| e.public_key.to_der == public_key }
MessageStruct.new(text: I18n.t('signed_with_trusted_certificate'), status: :success)
else
MessageStruct.new(text: I18n.t('signed_with_external_certificate'), status: :error)
end
end
def verify_contents(pkcs7, signed_data, trusted_certs)
return false if digest_algorithms(pkcs7).blank?
store = OpenSSL::X509::Store.new
store.set_default_paths
store.purpose = OpenSSL::X509::PURPOSE_SMIME_SIGN
store.verify_callback = ->(_success, _context) { true }
trusted_certs.each { |cert| store.add_cert(cert) }
pkcs7.verify(pkcs7.certificates, store, signed_data,
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
end
def digest_algorithms(pkcs7)
OpenSSL::ASN1.decode(pkcs7.to_der).value[1].value[0].value[1].value
end
def common_name(pkcs7)
cert = signer_certificate(pkcs7)
return if cert.nil?
cert.subject.to_a.assoc(COMMON_NAME)&.dig(1)
end
def signer_certificate(pkcs7)
info = pkcs7.signers.first
pkcs7.certificates&.find { |cert| cert.issuer == info.issuer && cert.serial == info.serial }
end
def certificate_chain(pkcs7)
signer = signer_certificate(pkcs7)
return [] if signer.nil?
certs = [signer]
while (issuer = pkcs7.certificates.find { |cert| cert.subject == certs.last.issuer })
break if certs.include?(issuer)
certs << issuer
end
certs.map { |cert| cert.subject.to_a.assoc(COMMON_NAME)&.dig(1) }
end
def signing_time(pkcs7, signature)
cms_signing_time(pkcs7) || parse_time(signature.time)
end
def cms_signing_time(pkcs7)
pkcs7.signers.first&.signed_time
rescue StandardError
nil
end
def parse_time(value)
return if value.blank?
time = value.delete("'").delete_prefix('D:')
offset = time[14..].to_s
Time.strptime("#{time.first(14)}#{offset.start_with?('+', '-') ? offset : '+0000'}", TIME_FORMAT)
end
def unsigned_changes?(document, io)
signed_end = document.signatures.map(&:signed_end).max
return false if document.trailer_ends.none? { |offset| offset > signed_end }
io.seek(0)
Pdfium::Document.open_bytes(io.read(signed_end)) do |signed_document|
next false unless signed_document.valid_cross_reference_table?
serialized_document(signed_document) != serialized_document(document)
end
end
def serialized_document(document)
pages = (0...document.page_count).map do |index|
page = document.get_page(index)
[page.rotation, page.objects, page.annotations, page.text]
end
[pages, document.bookmarks]
end
def signed_data(io, byte_range)
byte_range.each_slice(2).map do |offset, length|
io.seek(offset)
io.read(length)
end.join
end
end
+4 -3
View File
@@ -1153,9 +1153,10 @@ RSpec.describe 'Signing Form' do
click_on 'next' click_on 'next'
draw_canvas draw_canvas
expect do click_on 'Sign and Complete'
click_on 'Sign and Complete'
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1) expect(page).to have_content('Document has been signed!')
expect(ProcessSubmitterCompletionJob.jobs.size).to eq(1)
end end
end end