Compare commits

...
106 Commits
Author SHA1 Message Date
Alex Turchyn 47c090e1f1 Merge from docusealco/wip 2026-09-21 14:53:39 +03:00
Pete Matsyburka 678997fb36 adjust embed script 2026-09-21 09:06:26 +03:00
Pete Matsyburka 24f195c7e5 normalize extension 2026-09-21 09:06:25 +03:00
Pete Matsyburka 5741bb4db2 normalize page index 2026-09-20 17:37:02 +03:00
Pete Matsyburka 1d169668f0 fix ci 2026-09-20 14:58:06 +03:00
Pete Matsyburka ef3d5bec7e mcp use template access 2026-09-20 13:30:28 +03:00
Pete Matsyburka d578ca3cc9 only dss changes 2026-09-20 13:30:28 +03:00
Pete Matsyburka 5432978c0d adjust load vips 2026-09-20 13:30:27 +03:00
Pete Matsyburka de1af49348 fix variable highlight 2026-09-18 09:18:50 +03:00
Alex Turchyn 2f0bcb4a80 hide tooltips on mobile 2026-09-17 23:45:59 +03:00
Pete Matsyburka 11f27c8cf0 reset rotation 2026-09-14 16:06:16 +03:00
Alex Turchyn 6e2430e5a2 Merge from docusealco/wip 2026-09-14 15:54:52 +03:00
Pete Matsyburka 85fbae1813 validate webhook header 2026-09-14 15:07:56 +03:00
Pete Matsyburka 9941122d90 adjust i18n 2026-09-14 11:08:40 +03:00
Pete Matsyburka 71be6db358 adjust delegated values 2026-09-12 18:34:39 +03:00
Pete Matsyburka 994c1b3155 keep pasted field roles 2026-09-12 18:29:56 +03:00
Pete Matsyburka 92b297a620 fix friendly name 2026-09-11 13:00:26 +03:00
Alex Turchyn bd03498b0f adjust show archived 2026-09-11 09:56:10 +03:00
Pete Matsyburka 660b24710e do not email archived user 2026-09-10 19:17:27 +03:00
Pete Matsyburka 1416cf72e8 sms retry 30 seconds 2026-09-10 15:29:27 +03:00
Alex Turchyn ae7b0ce608 fix settings collapse spacing 2026-09-09 16:09:19 +03:00
Pete Matsyburka a3828ec390 xlsx write native boolean 2026-09-09 13:35:20 +03:00
Pete Matsyburka b8c1691bd7 optimize xlsx export 2026-09-09 12:46:28 +03:00
Pete Matsyburka 4b00c896e4 reset signature on delegate 2026-09-08 18:52:12 +03:00
Pete Matsyburka 781da32a07 replace filename slashes 2026-09-08 13:53:35 +03:00
Pete Matsyburka 307036eae6 fix invite party 2026-09-08 09:12:47 +03:00
Pete Matsyburka 9944a9b1a5 adjust bind address 2026-09-07 17:57:07 +03:00
Alex Turchyn 11856a17ac Merge from docusealco/wip 2026-09-07 12:53:23 +03:00
Pete Matsyburka 7b4c74d711 safe page cycle 2026-09-06 19:25:34 +03:00
Pete Matsyburka e0b0d4e803 add authorize 2026-09-06 19:25:34 +03:00
Pete Matsyburka ec04a95e36 fix pdf validation 2026-09-06 09:58:19 +03:00
Pete Matsyburka 887d80f6d7 raw page size 2026-09-05 13:09:09 +03:00
Pete Matsyburka 9ad281d457 handle 2fa email bounce 2026-09-04 09:01:51 +03:00
Pete Matsyburka e316d2006f subject only email message 2026-09-03 18:59:33 +03:00
Pete Matsyburka f9669b0e16 process pdf v2 2026-09-03 11:48:54 +03:00
Pete Matsyburka 785eb789ad fix undo autosave false 2026-09-02 20:42:08 +03:00
Pete Matsyburka 436d947f6a detect fields autosave false 2026-09-02 20:08:42 +03:00
Pete Matsyburka 033ce3a169 fix text align 2026-09-02 20:02:57 +03:00
Alex Turchyn c216e43d24 Merge from docusealco/wip 2026-08-31 11:35:27 +03:00
Pete Matsyburka d2373c1578 fix normalize email 2026-08-31 10:13:17 +03:00
Pete Matsyburka 474603e1c9 hide list form buttons 2026-08-30 18:51:35 +03:00
Pete Matsyburka 1fc1444b8f optimize load bmp 2026-08-30 15:49:54 +03:00
Pete Matsyburka 47361da26f adjust validate required 2026-08-30 15:36:49 +03:00
Pete Matsyburka 11dea042ad adjust test mode auth 2026-08-30 12:07:05 +03:00
Pete Matsyburka 8756931638 render locked page 2026-08-30 12:01:54 +03:00
Pete Matsyburka 180b201dcf fix mfa rate limit 2026-08-30 12:01:54 +03:00
Pete Matsyburka a1c30c2093 update tools verify 2026-08-30 11:04:45 +03:00
Pete Matsyburka 66bf6af7ba adjust verify pdf signature 2026-08-30 10:56:34 +03:00
Pete Matsyburka 1a65e7bcff adjust download route 2026-08-29 21:53:01 +03:00
Pete Matsyburka 59b5f03948 fix buttons order 2026-08-29 20:18:34 +03:00
Pete Matsyburka 4626518c83 fix search 2026-08-29 20:06:14 +03:00
Pete Matsyburka c18e263f34 adjust verify pdf signature 2026-08-29 20:06:13 +03:00
Pete Matsyburka c3dae1bfe4 add size checks 2026-08-29 16:29:52 +03:00
Pete Matsyburka b44152fd52 fix search index 2026-08-29 16:29:52 +03:00
Pete Matsyburka 2889a7c937 squish name 2026-08-29 16:29:52 +03:00
Pete Matsyburka 91e65e4a19 adjust prefill 2026-08-29 16:29:52 +03:00
Pete Matsyburka 2a1a2cbd82 fix generation 2026-08-28 19:15:48 +03:00
Pete Matsyburka 99b3247cf7 archive account 2026-08-25 18:07:48 +03:00
Pete Matsyburka fa91c6d6be fix mobile filters 2026-08-25 15:12:19 +03:00
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
124 changed files with 2621 additions and 1022 deletions

No files matched your search

+15 -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,16 @@ 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/latest/download/pdfium-musl-$(uname -m).zip"
sudo apt-get install -y libvips liblept5 unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
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" cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so rm -rf pdfium-linux.zip /tmp/pdfium-linux
rm -f pdfium-linux.tgz
- 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/20260920/pdfium-musl-$(uname -m).zip" && \
case "$(uname -m)" in \ case "$(uname -m)" in \
x86_64) echo "2c953ff72ee2dda07e7fc577e25841cc3d6464468a7c5adfaea574efcbc3b90b pdfium-linux.zip" ;; \ x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
aarch64) echo "23bbe287d2753fdb05741c7660647eb0ef0d2e4da2ce0722bfa9d9d455bd64e2 pdfium-linux.zip" ;; \ aarch64) echo "73b72ed79b9bfdd494dee7e863482c24971a8d4b11f6cd1289aa170d31304a1f 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
+2 -1
View File
@@ -36,7 +36,7 @@ gem 'rotp'
gem 'rouge', require: false gem 'rouge', require: false
gem 'rqrcode', require: false gem 'rqrcode', require: false
gem 'ruby-vips' gem 'ruby-vips'
gem 'rubyXL', require: false gem 'rubyzip', require: false
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq' gem 'sidekiq'
gem 'sqlite3', require: false gem 'sqlite3', require: false
@@ -45,6 +45,7 @@ gem 'trilogy', require: false
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
gem 'xlsxtream', require: false
group :development, :test do group :development, :test do
gem 'better_html' gem 'better_html'
+6 -5
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)
@@ -497,9 +497,6 @@ GEM
ruby-vips (2.3.0) ruby-vips (2.3.0)
ffi (~> 1.12) ffi (~> 1.12)
logger logger
rubyXL (3.4.35)
nokogiri (>= 1.10.8)
rubyzip (>= 3.2.2)
rubyzip (3.2.2) rubyzip (3.2.2)
securerandom (0.4.1) securerandom (0.4.1)
semantic_range (3.1.1) semantic_range (3.1.1)
@@ -575,10 +572,13 @@ GEM
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yaml (0.4.0) yaml (0.4.0)
zeitwerk (2.8.2) zeitwerk (2.8.2)
zip_kit (6.3.4)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@@ -638,7 +638,7 @@ DEPENDENCIES
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
ruby-vips ruby-vips
rubyXL rubyzip
shakapacker shakapacker
sidekiq sidekiq
simplecov simplecov
@@ -650,6 +650,7 @@ DEPENDENCIES
tzinfo-data tzinfo-data
web-console web-console
webmock webmock
xlsxtream
RUBY VERSION RUBY VERSION
ruby 4.0.5 ruby 4.0.5
+13 -9
View File
@@ -23,17 +23,20 @@ class AccountsController < ApplicationController
unless Docuseal.multitenant? unless Docuseal.multitenant?
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, @encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::APP_URL_KEY) key: EncryptedConfig::APP_URL_KEY)
@encrypted_config.assign_attributes(app_url_params)
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS]) if can?(:manage, @encrypted_config)
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url')) @encrypted_config.assign_attributes(app_url_params)
return render :show, status: :unprocessable_content unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_content
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end end
with_locale do with_locale do
@@ -47,7 +50,8 @@ class AccountsController < ApplicationController
authorize!(:manage, current_account) authorize!(:manage, current_account)
true_user.skip_reconfirmation! true_user.skip_reconfirmation!
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@')) true_user.update!(locked_at: Time.current, archived_at: Time.current,
email: true_user.email.sub('@', '+removed@'))
true_user.account.update!(archived_at: Time.current) true_user.account.update!(archived_at: Time.current)
# rubocop:disable Layout/LineLength # rubocop:disable Layout/LineLength
@@ -23,7 +23,7 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid) blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase) if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar) Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content return head :unprocessable_content
@@ -19,7 +19,7 @@ module Api
return head :not_found unless blob return head :not_found unless blob
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase) if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar) Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content return head :unprocessable_content
+6 -7
View File
@@ -17,24 +17,23 @@ module Api
def verify def verify
file = Base64.decode64(params[:file]) file = Base64.decode64(params[:file])
pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account) trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file))) is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
render json: { render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found', checksum_status: is_checksum_found ? 'verified' : 'not_found',
signatures: pdf.signatures.map do |sig| signatures: VerifyPdfSignature.call(StringIO.new(file), trusted_certs).map do |sig|
{ {
verification_result: sig.verify(trusted_certs:).messages, verification_result: sig.messages.map { |m| { type: m.status || :info, content: m.text } },
signer_name: sig.signer_name, signer_name: sig.common_name,
signing_reason: sig.signing_reason, signing_reason: sig.reason,
signing_time: sig.signing_time, signing_time: sig.signing_time,
signature_type: sig.signature_type signature_type: sig.type
} }
end end
} }
rescue HexaPDF::MalformedPDFError rescue Pdfium::PdfiumError
render json: { error: 'Malformed PDF' }, status: :unprocessable_content render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end end
end end
+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?
@@ -46,6 +46,8 @@ module Mcp
authorize!(:create, @template) authorize!(:create, @template)
Templates.maybe_assign_access(@template)
if mcp_params['url'].present? if mcp_params['url'].present?
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
+4 -4
View File
@@ -14,14 +14,14 @@ class MfaSetupController < ApplicationController
def edit; end def edit; end
def create def create
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
if current_user.validate_and_consume_otp!(params[:otp_attempt]) if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.otp_required_for_login = true current_user.otp_required_for_login = true
current_user.save! current_user.save!
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured') redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
else else
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name) @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = I18n.t('code_is_invalid') @error_message = I18n.t('code_is_invalid')
@@ -31,13 +31,13 @@ class MfaSetupController < ApplicationController
end end
def destroy def destroy
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
if current_user.validate_and_consume_otp!(params[:otp_attempt]) if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(otp_required_for_login: false, otp_secret: nil) current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed') redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
else else
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
@error_message = I18n.t('code_is_invalid') @error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
@@ -26,8 +26,9 @@ class PreviewDocumentPageController < ActionController::API
@template = attachment.record @template = attachment.record
preview_image = attachment.preview_images.joins(:blob) preview_image =
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] }) attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id].to_i}.png", "#{params[:id].to_i}.jpg"] })
if preview_image if preview_image
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),
+48 -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,31 @@ 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::BouncedEmail => e
@error_message = e.message
render :error, 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
@@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController class StartFormEmail2faSendController < ApplicationController
layout 'form'
around_action :with_browser_locale around_action :with_browser_locale
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
@@ -13,12 +15,20 @@ class StartFormEmail2faSendController < ApplicationController
@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]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)), redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
**redir_params **redir_params
rescue Submitters::BouncedEmail => e
@error_message = e.message
render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode => e rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)), redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message alert: e.message
@@ -36,14 +46,24 @@ class StartFormEmail2faSendController < ApplicationController
return redirect_to start_form_path(@template.slug) if is_archived return redirect_to start_form_path(@template.slug) if is_archived
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) && return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
@template.preferences['shared_link_2fa'] == true (@template.preferences['shared_link_2fa'] == true || submitter_exists?(@template, params))
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) Rollbar.warning("Not 2FA shared template: #{@template.id}") if defined?(Rollbar)
redirect_to start_form_path(@template.slug) redirect_to start_form_path(@template.slug)
end 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,103 @@
# 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::BouncedEmail => e
@error_message = e.message
render 'start_form/error', 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
@@ -48,7 +48,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
private private
def submitter_slug def submitter_slug
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id] params[:submit_form_slug] || params[:submitter_slug]
end end
def respond_with_combined(submitter) def respond_with_combined(submitter)
@@ -107,6 +107,8 @@ class SubmitFormController < ApplicationController
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug]) submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
@submitter = submitter_version.submitter @submitter = submitter_version.submitter
maybe_render_locked_page
end end
private private
@@ -19,14 +19,15 @@ 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
user = @submitter.submission.created_by_user || @submitter.template.author user = @submitter.submission.created_by_user || @submitter.template.author
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false if Users.send_emails?(user) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(@submitter, user).deliver_later! SubmitterMailer.declined_email(@submitter, user).deliver_later!
end end
@@ -32,7 +32,8 @@ class SubmitFormDelegateController < ApplicationController
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request, SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
{ old_email: @submitter.email, email: }) { old_email: @submitter.email, email: })
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14)) @submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14),
values: Submitters.fetch_values_for_delegate(@submitter))
end end
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id) SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@@ -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
@@ -37,6 +35,12 @@ class SubmitFormEmail2fasController < ApplicationController
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true) RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
if Docuseal.multitenant? && Submitters.email_bounced_recently?(@submitter.email)
Rollbar.warning("Bounced OTP email for submitter: #{@submitter.id}") if defined?(Rollbar)
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:verification_email_bounced)
end
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s) SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {} redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
@@ -23,7 +23,7 @@ class SubmitFormInviteController < ApplicationController
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id) @submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid }) SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: attrs[:uuid] })
end end
@submitter.submission.update!(submitters_order: :preserved) @submitter.submission.update!(submitters_order: :preserved)
@@ -13,9 +13,10 @@ class TemplatesDebugController < ApplicationController
data = attachment.download data = attachment.download
unless attachment.image? unless attachment.image?
pdf = HexaPDF::Document.new(io: StringIO.new(data)) fields =
Pdfium::Document.open_io(StringIO.new(data)) do |doc|
fields = Templates::FindAcroFields.call(pdf, attachment, data) Templates::FindPdfiumAcroFields.call(attachment, doc, data)
end
end end
# fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank? # fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
@@ -10,8 +10,14 @@ class TemplatesDetectFieldsController < ApplicationController
sse = SSE.new(response.stream) sse = SSE.new(response.stream)
documents = @template.schema_documents.preload(:blob) documents =
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present? if params[:attachment_uuid].present?
@template.documents.where(uuid: params[:attachment_uuid])
else
@template.schema_documents
end
documents = documents.preload(:blob)
page_number = params[:page].presence&.to_i page_number = params[:page].presence&.to_i
@@ -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
@@ -61,7 +63,7 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url], validate: true).body) tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
tempfile.rewind tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present? filename = URI.decode_www_form_component(params[:filename]).tr('/', '-') if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url])) filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
@@ -6,6 +6,7 @@ class TestingAccountsController < ApplicationController
def create def create
authorize!(:manage, current_account) authorize!(:manage, current_account)
authorize!(:manage, current_user) authorize!(:manage, current_user)
authorize!(:manage, EncryptedConfig)
impersonate_user(Accounts.find_or_create_testing_user(true_user.account)) impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
+2 -4
View File
@@ -11,11 +11,9 @@ class UserInitialsController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank? return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase extension = FilenameUtils.dangerous_extension(file.original_filename)
if Submitters::DANGEROUS_EXTENSIONS.include?(extension) raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
@@ -11,11 +11,9 @@ class UserSignaturesController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank? return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase extension = FilenameUtils.dangerous_extension(file.original_filename)
if Submitters::DANGEROUS_EXTENSIONS.include?(extension) raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
@@ -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
+9 -1
View File
@@ -3,11 +3,19 @@
class WebhookSecretController < ApplicationController class WebhookSecretController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false load_and_authorize_resource :webhook_url, parent: false
HEADER_NAME_REGEXP = /\A[\w-]+\z/
def show; end def show; end
def update def update
key = webhook_secret_params[:key]
if key.present? && !HEADER_NAME_REGEXP.match?(key)
return redirect_back(fallback_location: settings_webhook_path(@webhook_url), alert: I18n.t('unable_to_save'))
end
@webhook_url.update!(secret: { @webhook_url.update!(secret: {
webhook_secret_params[:key] => webhook_secret_params[:value] key => webhook_secret_params[:value]
}.compact_blank) }.compact_blank)
redirect_back(fallback_location: settings_webhook_path(@webhook_url), redirect_back(fallback_location: settings_webhook_path(@webhook_url),
+1 -1
View File
@@ -353,7 +353,7 @@ function buildExtensions ({ Node, Mark, Extension, Plugin, Decoration, Decoratio
const buildDecorations = (doc) => { const buildDecorations = (doc) => {
const decorations = [] const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => { doc.descendants((node, pos) => {
if (!node.isText) return if (!node.isText) return
+1 -1
View File
@@ -150,7 +150,7 @@ export default actionable(targetable(class extends HTMLElement {
const buildDecorations = (doc) => { const buildDecorations = (doc) => {
const decorations = [] const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => { doc.descendants((node, pos) => {
if (!node.isText) return if (!node.isText) return
+1 -1
View File
@@ -230,7 +230,7 @@ export default {
download () { download () {
this.isDownloading = true this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, { fetch(this.baseUrl + `/s/${this.submitterSlug}/documents`, {
method: 'GET', method: 'GET',
...this.fetchOptions ...this.fetchOptions
}).then(async (response) => { }).then(async (response) => {
+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()
@@ -261,7 +261,7 @@ export default {
} }
}, },
resendCode () { resendCode () {
if (this.codeSentAt && Date.now() - this.codeSentAt < 15000) { if (this.codeSentAt && Date.now() - this.codeSentAt < 30000) {
this.startResendCodeCountdown() this.startResendCodeCountdown()
} else { } else {
this.isResendLoading = true this.isResendLoading = true
@@ -274,7 +274,7 @@ export default {
} }
}, },
startResendCodeCountdown () { startResendCodeCountdown () {
this.resendCodeCountdown = 15 - parseInt((Date.now() - this.codeSentAt) / 1000) this.resendCodeCountdown = 30 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.interval = setInterval(() => { this.interval = setInterval(() => {
this.resendCodeCountdown-- this.resendCodeCountdown--
@@ -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) {
+19 -11
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"
@@ -2377,7 +2371,6 @@ export default {
const areaCopy = JSON.parse(JSON.stringify(area)) const areaCopy = JSON.parse(JSON.stringify(area))
delete fieldCopy.areas delete fieldCopy.areas
delete fieldCopy.submitter_uuid
areaCopy.relativeX = area.x - minX areaCopy.relativeX = area.x - minX
areaCopy.relativeY = area.y - minY areaCopy.relativeY = area.y - minY
@@ -2387,6 +2380,7 @@ export default {
const clipboardData = { const clipboardData = {
items, items,
submitters: this.template.submitters.map((submitter) => ({ uuid: submitter.uuid, name: submitter.name })),
templateId: this.template.id, templateId: this.template.id,
timestamp: Date.now(), timestamp: Date.now(),
isGroup: true isGroup: true
@@ -2497,6 +2491,20 @@ export default {
const fieldUuidIndex = {} const fieldUuidIndex = {}
const fieldOptionsMap = {} const fieldOptionsMap = {}
const submitterUuidsMap = {}
const copiedSubmitterUuids = [...new Set(data.items.map((item) => item.field.submitter_uuid).filter(Boolean))]
if (copiedSubmitterUuids.length > 1) {
copiedSubmitterUuids.forEach((uuid) => {
const name = data.submitters?.find((submitter) => submitter.uuid === uuid)?.name
const submitter = this.template.submitters.find((s) => s.uuid === uuid) ||
(name && this.template.submitters.find((s) => s.name.toLowerCase() === name.toLowerCase()))
submitterUuidsMap[uuid] = (submitter || this.selectedSubmitter).uuid
})
}
data.items.forEach((item) => { data.items.forEach((item) => {
const field = JSON.parse(JSON.stringify(item.field)) const field = JSON.parse(JSON.stringify(item.field))
@@ -2521,7 +2529,7 @@ export default {
const newField = fieldUuidIndex[field.uuid] || { const newField = fieldUuidIndex[field.uuid] || {
...field, ...field,
uuid: v4(), uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid, submitter_uuid: submitterUuidsMap[field.submitter_uuid] || this.selectedSubmitter.uuid,
areas: [] areas: []
} }
@@ -3531,6 +3539,8 @@ export default {
this.onChange(this.template) this.onChange(this.template)
} }
this.pushUndo()
if (!this.autosave && !force) { if (!this.autosave && !force) {
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -3541,8 +3551,6 @@ export default {
} }
}) })
this.pushUndo()
return this.baseFetch(`/templates/${this.template.id}`, { return this.baseFetch(`/templates/${this.template.id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
+8 -5
View File
@@ -555,8 +555,10 @@ export default {
return this.template.schema.some((item) => item.dynamic) return this.template.schema.some((item) => item.dynamic)
}, },
numberOfPages () { numberOfPages () {
return this.template.documents.reduce((acc, doc) => { return this.template.schema.reduce((acc, item) => {
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length const doc = this.template.documents.find((d) => d.uuid === item.attachment_uuid)
return acc + (doc?.metadata?.pdf?.number_of_pages || doc?.preview_images?.length || 0)
}, 0) }, 0)
}, },
isShowFieldSearch () { isShowFieldSearch () {
@@ -774,9 +776,10 @@ export default {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
...(this.withDetectExistingFields body: JSON.stringify({
? { body: JSON.stringify({ fields: this.buildExistingFields() }) } attachment_uuid: this.template.schema.map((item) => item.attachment_uuid),
: {}) ...(this.withDetectExistingFields ? { fields: this.buildExistingFields() } : {})
})
}).then(async (response) => { }).then(async (response) => {
const reader = response.body.getReader() const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8') const decoder = new TextDecoder('utf-8')
@@ -366,6 +366,9 @@ export default {
} }
} }
}, },
beforeUnmount () {
document.getElementById('list_form_buttons')?.classList?.add('hidden')
},
methods: { methods: {
t (key) { t (key) {
return this.i18n[key] || key return this.i18n[key] || key
-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
+14 -10
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' && Users.send_emails?(user) &&
(!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
View File
@@ -31,6 +31,8 @@ class EmailMessage < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
normalizes :body, with: ->(value) { value.to_s }, apply_to_nil: true
before_validation :set_sha1, on: :create before_validation :set_sha1, on: :create
def normalized_body def normalized_body
+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, {
+1 -1
View File
@@ -90,7 +90,7 @@ class Submitter < ApplicationRecord
def friendly_name def friendly_name
if name.present? && email.present? && email.exclude?(',') if name.present? && email.present? && email.exclude?(',')
%("#{name.delete('"')}" <#{email}>) %("#{name.delete('"').squish}" <#{email}>)
else else
email email
end end
@@ -3,7 +3,7 @@
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %> <%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %>
<span class="flex items-center"> <span class="flex items-center">
<span class="whitespace-nowrap"><%= t('docuseal_trusted_signature') %></span> <span class="whitespace-nowrap"><%= t('docuseal_trusted_signature') %></span>
<div class="tooltip tooltip-top ml-1 shrink-0" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>"> <div class="tooltip tooltip-top ml-1 shrink-0 hidden md:flex" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1 shrink-0') %> <%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1 shrink-0') %>
</div> </div>
</span> </span>
@@ -14,7 +14,7 @@
</a> </a>
</td> </td>
<td> <td>
<div class="tooltip" data-tip="<%= t('unlock_with_docuseal_pro') %>"> <div class="md:tooltip" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= button_to settings_esign_path, method: :put, params: { name: Docuseal::AATL_CERT_NAME }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: t('make_default'), disabled: true do %> <%= button_to settings_esign_path, method: :put, params: { name: Docuseal::AATL_CERT_NAME }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: t('make_default'), disabled: true do %>
<%= t('make_default') %> <%= t('make_default') %>
<% end %> <% end %>
+1 -1
View File
@@ -120,7 +120,7 @@
<span> <span>
<%= t('timeserver_url') %> <%= t('timeserver_url') %>
</span> </span>
<span class="tooltip" data-tip="<%= t('url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures') %>"> <span class="tooltip hidden md:flex" data-tip="<%= t('url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</span> </span>
+4 -6
View File
@@ -6,12 +6,10 @@
<%= t('mcp_server') %> <%= t('mcp_server') %>
</h1> </h1>
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center"> <div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
<div class="tooltip"> <%= link_to new_settings_mcp_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= link_to new_settings_mcp_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %> <%= svg_icon('plus', class: 'w-6 h-6') %>
<%= svg_icon('plus', class: 'w-6 h-6') %> <span><%= t('new_token') %></span>
<span><%= t('new_token') %></span> <% end %>
<% end %>
</div>
</div> </div>
</div> </div>
<% if @mcp_token.present? %> <% if @mcp_token.present? %>
@@ -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>
@@ -6,11 +6,16 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= 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 hidden md:flex" 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 %>
@@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_BUTTON_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_BUTTON_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %> <%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
<div class="form-control"> <div class="form-control">
@@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_MESSAGE_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_MESSAGE_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:title, :body).new(*(f.object.value || {}).values_at('title', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:title, :body).new(*(f.object.value || {}).values_at('title', 'body')) do |ff| %>
<div class="form-control"> <div class="form-control">
@@ -7,7 +7,7 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <div class="form-control">
<autoresize-textarea> <autoresize-textarea>
@@ -6,11 +6,16 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= 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 hidden md:flex" 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 %>
@@ -6,13 +6,13 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %>
<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 hidden md:flex" 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>
+31
View File
@@ -0,0 +1,31 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
<p dir="auto" class="text-sm"><%= t('invited_by_html', name: @template.account.name) %></p>
</div>
</div>
</div>
</div>
<div class="alert items-start bg-base-100 border border-base-300">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<div class="text-sm"><%= @error_message %></div>
</div>
<div dir="auto" class="form-control">
<%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %>
</div>
</div>
</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') %>
+1 -1
View File
@@ -36,7 +36,7 @@
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %> <%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</span> </span>
<% if (device = DetectBrowserDevice.call(event.data['ua'])) %> <% if (device = DetectBrowserDevice.call(event.data['ua'])) %>
<span class="tooltip tooltip-top" data-tip="<%= t(device) %>"> <span class="md:tooltip tooltip-top" data-tip="<%= t(device) %>">
<%= svg_icon("device_#{device}", class: 'w-4 h-4') %> <%= svg_icon("device_#{device}", class: 'w-4 h-4') %>
</span> </span>
<% end %> <% end %>
@@ -7,7 +7,7 @@
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %> <%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: last_submission&.submitters_order.in?(['preserved', nil]) %> <%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: last_submission&.submitters_order.in?(['preserved', nil]) %>
<span class="label"><%= t('preserve_order') %></span> <span class="label"><%= t('preserve_order') %></span>
<span class="tooltip" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>"> <span class="tooltip hidden md:flex" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
<% end %> <% end %>
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex items-center md:items-end gap-2"> <div class="flex items-center md:items-end gap-2">
<%= render 'submissions_filters/applied_filters', filter_params:, with_status: true %> <%= render 'submissions_filters/applied_filters', filter_params:, with_status: true, with_default_status: true %>
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>
</div> </div>
</div> </div>
@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<% view_archived_html = capture do %> <% view_archived_html = capture do %>
<% if can?(:manage, :countless) || current_account.submissions.where.not(archived_at: nil).exists? %> <% if can?(:manage, :countless) || current_account.submissions.archived.exists? || current_account.templates.where(Submission.where(Submission.arel_table[:template_id].eq(::Template.arel_table[:id])).arel.exists).archived.exists? %>
<div> <div>
<a href="<%= submissions_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a> <a href="<%= submissions_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
</div> </div>
@@ -3,15 +3,16 @@
<% ordered_filters = request.query_parameters.keys.filter_map { |key| filter_names.find { |name| key.start_with?(name) } }.uniq %> <% ordered_filters = request.query_parameters.keys.filter_map { |key| filter_names.find { |name| key.start_with?(name) } }.uniq %>
<% chip_order = ordered_filters.reverse.each_with_index.to_h { |name, index| [name, index + 1] } %> <% chip_order = ordered_filters.reverse.each_with_index.to_h { |name, index| [name, index + 1] } %>
<% chip_order.default = ordered_filters.size + 1 %> <% chip_order.default = ordered_filters.size + 1 %>
<% status_icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
<% status_icons = { 'all' => 'list', 'pending' => 'clock', 'completed' => 'circle_check' } %> <% status_icons = { 'all' => 'list', 'pending' => 'clock', 'completed' => 'circle_check' } %>
<% current_status = status_icons.key?(params[:status].to_s) ? params[:status].to_s : 'all' %> <% current_status = status_icons.key?(params[:status].to_s) ? params[:status].to_s : 'all' %>
<% status_icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
<% default_status_icon = status_icons[params[:status]] if status_icon.blank? && local_assigns[:with_default_status] && current_status != 'all' %>
<% with_status_button = local_assigns[:with_status] && status_icon.blank? %> <% with_status_button = local_assigns[:with_status] && status_icon.blank? %>
<% chips_html = capture do %> <% status_chip_html = capture do %>
<% if status_icon %> <% if status_icon || default_status_icon %>
<div class="order-none flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700"> <div class="order-none flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700 <%= 'max-md:hidden' if default_status_icon %>">
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
<%= svg_icon(status_icon, class: 'w-5 h-5 shrink-0') %> <%= svg_icon(status_icon || default_status_icon, class: 'w-5 h-5 shrink-0') %>
<span class="font-normal truncate"><%= t(params[:status]) %></span> <span class="font-normal truncate"><%= t(params[:status]) %></span>
<% end %> <% end %>
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %> <%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
@@ -19,6 +20,8 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% end %>
<% chips_html = capture do %>
<% if params[:folder].present? %> <% if params[:folder].present? %>
<div class="order-<%= chip_order['folder'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('folder') %>"> <div class="order-<%= chip_order['folder'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('folder') %>">
<%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
@@ -78,7 +81,8 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% if with_status_button && chips_html.blank? %> <% with_status_dropdown = with_status_button && chips_html.blank? %>
<% if with_status_dropdown %>
<div class="dropdown md:hidden shrink-0"> <div class="dropdown md:hidden shrink-0">
<label tabindex="0" class="cursor-pointer flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>"> <label tabindex="0" class="cursor-pointer flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>">
<%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %> <%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %>
@@ -122,15 +126,16 @@
</ul> </ul>
</div> </div>
<% end %> <% end %>
<% if chips_html.present? %> <% if chips_html.present? || status_chip_html.present? %>
<scroll-fade class="flex items-center gap-2 overflow-x-auto flex-1 min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar]:w-0 md:contents"> <scroll-fade class="flex items-center gap-2 overflow-x-auto flex-1 min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar]:w-0 md:contents <%= 'max-md:hidden' if with_status_dropdown %>">
<% if with_status_button %> <% if with_status_button && !with_status_dropdown %>
<a href="<%= submissions_filter_path('status', query_params.merge(path: url_for)) %>" data-turbo-frame="modal" class="order-none md:hidden flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl shrink-0 <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>"> <a href="<%= submissions_filter_path('status', query_params.merge(path: url_for)) %>" data-turbo-frame="modal" class="order-none md:hidden flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl shrink-0 <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>">
<%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %> <%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %>
<span><%= t(current_status) %></span> <span><%= t(current_status) %></span>
<%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %> <%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %>
</a> </a>
<% end %> <% end %>
<%= status_chip_html %>
<%= chips_html %> <%= chips_html %>
</scroll-fade> </scroll-fade>
<% end %> <% end %>
@@ -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' %>
+2 -2
View File
@@ -56,7 +56,7 @@
<div class="mockup-code overflow-hidden pb-0 mt-4"> <div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex"> <span class="top-0 right-0 absolute flex">
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %> <%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script> <clipboard-copy data-text="<script src=&quot;<%= Docuseal.multitenant? ? "#{Docuseal::CDN_URL}/js/form.js" : embed_script_url(filename: 'form.js') %>&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form> <docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
"> ">
@@ -78,7 +78,7 @@
</clipboard-copy> </clipboard-copy>
</span> </span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span> <pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal.multitenant? ? "#{Docuseal::CDN_URL}/js/form.js" : embed_script_url(filename: 'form.js') %>"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span> <span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre> </code></pre>
+3 -3
View File
@@ -35,14 +35,14 @@
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex flex-col md:flex-row md:items-center gap-3"> <span class="flex flex-col md:flex-row md:items-center gap-3">
<% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %> <% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="md:tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t('expired') %> <%= t('expired') %>
</span> </span>
</div> </div>
<% else %> <% else %>
<a href="<%= submission_path(submission) %>" class="flex z-[1]"> <a href="<%= submission_path(submission) %>" class="flex z-[1]">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold md:tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<%= t(submitter.status) %> <%= t(submitter.status) %>
</span> </span>
</a> </a>
@@ -125,7 +125,7 @@
</span> </span>
</a> </a>
<% elsif submission.expired? %> <% elsif submission.expired? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="md:tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t('expired') %> <%= t('expired') %>
</span> </span>
+6 -5
View File
@@ -64,8 +64,12 @@
<% end %> <% end %>
</div> </div>
<div class="flex flex-wrap gap-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1"> <div class="flex flex-wrap gap-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1">
<% if !template.archived_at? && can?(:destroy, template) %> <% if can?(:destroy, template) %>
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving')[..-4], title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete %> <% if template.archived_at? %>
<%= button_to button_title(title: t('restore'), disabled_with: t('restoring')[..-4], icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1' %>
<% else %>
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving')[..-4], title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete %>
<% end %>
<% end %> <% end %>
<% if can?(:create, current_account.templates.new(author: current_user)) %> <% if can?(:create, current_account.templates.new(author: current_user)) %>
<div class="flex-1"> <div class="flex-1">
@@ -99,9 +103,6 @@
<% end %> <% end %>
<% end %> <% end %>
<% if template.archived_at? %> <% if template.archived_at? %>
<% if can?(:destroy, template) %>
<%= button_to button_title(title: t('restore'), disabled_with: t('restoring')[..-4], icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1' %>
<% end %>
<div class="flex-1"> <div class="flex-1">
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm w-full' do %> <%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm w-full' do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
+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 %>
@@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<div class="flex flex-nowrap md:flex-wrap md:justify-end items-center gap-2"> <div class="flex flex-nowrap md:flex-wrap md:justify-end items-center gap-2">
<%= render 'submissions_filters/applied_filters', filter_params:, with_status: true %> <%= render 'submissions_filters/applied_filters', filter_params:, with_status: true, with_default_status: true %>
<div class="hidden md:block shrink-0 md:order-first"> <div class="hidden md:block shrink-0 md:order-first">
<%= link_to new_template_submissions_export_path(@template, archived: true), class: 'btn btn-ghost text-base h-10 min-h-10', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submissions_export_path(@template, archived: true), class: 'btn btn-ghost text-base h-10 min-h-10', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
+1 -1
View File
@@ -1,4 +1,4 @@
<% has_archived = can?(:manage, :countless) || current_account.templates.where.not(archived_at: nil).exists? %> <% has_archived = can?(:manage, :countless) || current_account.templates.archived.exists? %>
<% show_dropzone = params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %> <% show_dropzone = params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<dashboard-dropzone> <dashboard-dropzone>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %> <%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %>
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name"> <toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name">
<div class="flex items-center justify-between mb-2.5"> <div class="flex items-center justify-between mb-2.5">
<label for="is_root_folder" class="flex items-center tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>"> <label for="is_root_folder" class="flex items-center md:tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>">
<%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %> <%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %>
<span class="flex items-center mt-1"> <span class="flex items-center mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %> <%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %>
@@ -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 hidden md:flex" 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 hidden md:flex" 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 hidden md:flex" 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 hidden md:flex" 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') %>
+1 -1
View File
@@ -10,7 +10,7 @@
<% end %> <% end %>
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %> <%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %> <% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %>
<label for="template_shared_link" class="tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>"> <label for="template_shared_link" class="md:tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>">
<span><%= t('enable_shared_link') %></span> <span><%= t('enable_shared_link') %></span>
<%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %> <%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %>
</label> </label>
+1 -1
View File
@@ -24,7 +24,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% else %> <% else %>
<div class="tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>"> <div class="md:tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>">
<%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %> <%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %> <%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span> <span><%= t('new_user') %></span>
+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 %>
+1 -1
View File
@@ -83,7 +83,7 @@
</div> </div>
<% if @webhook_events.present? || params[:status].present? %> <% if @webhook_events.present? || params[:status].present? %>
<div class="mt-6"> <div class="mt-6">
<h2 id="log" class="text-2xl md:text-3xl font-bold"><%= t('events_log') %></h2> <h2 id="log" class="text-2xl md:text-3xl font-bold"><%= t('event_log') %></h2>
<div class="tabs border-b mt-4"> <div class="tabs border-b mt-4">
<%= link_to t('all'), url_for(params: request.query_parameters.except('status', 'page')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> <%= link_to t('all'), url_for(params: request.query_parameters.except('status', 'page')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('succeeded'), url_for(params: request.query_parameters.except('page').merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> <%= link_to t('succeeded'), url_for(params: request.query_parameters.except('page').merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
+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 = ''
+1 -1
View File
@@ -105,6 +105,6 @@ if ENV['REDIS_URL'].to_s.empty?
redis_password = Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}") redis_password = Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}")
ENV['REDIS_URL'] = "redis://default:#{redis_password}@0.0.0.0:6379/0" ENV['REDIS_URL'] = "redis://default:#{redis_password}@127.0.0.1:16379/0"
ENV['LOCAL_REDIS_URL'] = ENV.fetch('REDIS_URL', nil) ENV['LOCAL_REDIS_URL'] = ENV.fetch('REDIS_URL', nil)
end end
+1 -1
View File
@@ -2,7 +2,7 @@
autoload :CSV, 'csv' autoload :CSV, 'csv'
autoload :CSVSafe, 'csv-safe' autoload :CSVSafe, 'csv-safe'
autoload :RubyXL, 'rubyXL' autoload :Xlsxtream, 'xlsxtream'
autoload :Zip, 'zip' autoload :Zip, 'zip'
autoload :Numo, 'numo/narray' autoload :Numo, 'numo/narray'
autoload :OnnxRuntime, 'onnxruntime' autoload :OnnxRuntime, 'onnxruntime'
+61
View File
@@ -46,6 +46,20 @@ module HexaPDF
def color_space(name) def color_space(name)
GlobalConfiguration.constantize('color_space.map', name).new GlobalConfiguration.constantize('color_space.map', name).new
end end
def [](name)
return super unless value[name].nil? && INHERITABLE_FIELDS.include?(name)
seen = Set.new.compare_by_identity
seen << value
node = self
while node.value[name].nil? && (parent = node[:Parent]) && seen.add?(parent.value)
node = parent
end
node == self || node.value[name].nil? ? super : node[name]
end
end end
# fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field # fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field
@@ -112,6 +126,8 @@ module HexaPDF
end end
module CycleSafeInheritedValue module CycleSafeInheritedValue
using HexaPDF::Type::AcroForm::Field::HashRefinement
def inherited_value(field, name) def inherited_value(field, name)
seen = Set.new.compare_by_identity seen = Set.new.compare_by_identity
seen << field.value seen << field.value
@@ -167,8 +183,53 @@ module HexaPDF
end end
end end
end end
module CycleSafePageAncestors
def index
acyclic_ancestors? ? super : 0
end
def ancestor_nodes
acyclic_ancestors? ? super : []
end
private
def perform_validation
super if acyclic_ancestors?
end
def acyclic_ancestors?
seen = Set.new.compare_by_identity
node = self
node = node[:Parent] while node && seen.add?(node.value)
node.nil?
end
end
module CycleSafeOutlineEndpoints
private
def perform_validation
first = value[:First]
last = value[:Last]
if (first && !last) || (!first && last)
node, dir = first ? [self[:First], :Next] : [self[:Last], :Prev]
seen = Set.new.compare_by_identity
node = node[dir] while node && seen.add?(node.value)
return if node
end
super
end
end
end end
HexaPDF::Type::AcroForm::Field.singleton_class.prepend(HexaPDF::CycleSafeInheritedValue) HexaPDF::Type::AcroForm::Field.singleton_class.prepend(HexaPDF::CycleSafeInheritedValue)
HexaPDF::Type::AcroForm::Field.prepend(HexaPDF::CycleSafeFullFieldName) HexaPDF::Type::AcroForm::Field.prepend(HexaPDF::CycleSafeFullFieldName)
HexaPDF::Type::AcroForm::Form.prepend(HexaPDF::CycleSafeEachField) HexaPDF::Type::AcroForm::Form.prepend(HexaPDF::CycleSafeEachField)
HexaPDF::Type::Page.prepend(HexaPDF::CycleSafePageAncestors)
HexaPDF::Type::Outline.prepend(HexaPDF::CycleSafeOutlineEndpoints)
HexaPDF::Type::OutlineItem.prepend(HexaPDF::CycleSafeOutlineEndpoints)
+113 -1
View File
@@ -228,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
@@ -596,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.
@@ -786,6 +790,7 @@ en: &en
the_code_has_been_sent_to_your_email: The code has been sent to your email. the_code_has_been_sent_to_your_email: The code has been sent to your email.
enter_the_verification_code_from_your_email: Enter the verification code from your email. enter_the_verification_code_from_your_email: Enter the verification code from your email.
too_many_attempts: Too many attempts. too_many_attempts: Too many attempts.
verification_email_bounced: "Unable to send the verification code: email to this address bounced."
verification_code: Verification Code verification_code: Verification Code
resend_code: Resend Code resend_code: Resend Code
verify_new_sign_in: Verify new sign in verify_new_sign_in: Verify new sign in
@@ -988,6 +993,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.
@@ -1003,6 +1015,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}'
@@ -1338,6 +1351,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
@@ -1465,6 +1479,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
@@ -1706,6 +1727,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.
@@ -1893,6 +1917,7 @@ es: &es
the_code_has_been_sent_to_your_email: El código ha sido enviado a tu correo electrónico. the_code_has_been_sent_to_your_email: El código ha sido enviado a tu correo electrónico.
enter_the_verification_code_from_your_email: Ingresa el código de verificación de tu correo electrónico. enter_the_verification_code_from_your_email: Ingresa el código de verificación de tu correo electrónico.
too_many_attempts: Demasiados intentos. too_many_attempts: Demasiados intentos.
verification_email_bounced: "No se pudo enviar el código de verificación: el correo a esta dirección rebotó."
verification_code: Código de Verificación verification_code: Código de Verificación
resend_code: Reenviar Código resend_code: Reenviar Código
verify_new_sign_in: Verificar nuevo inicio de sesión verify_new_sign_in: Verificar nuevo inicio de sesión
@@ -2110,6 +2135,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}'
@@ -2459,6 +2485,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
@@ -2586,6 +2613,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
@@ -2827,6 +2861,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."
@@ -3014,6 +3051,7 @@ it: &it
the_code_has_been_sent_to_your_email: Il codice è stato inviato alla tua e-mail. the_code_has_been_sent_to_your_email: Il codice è stato inviato alla tua e-mail.
enter_the_verification_code_from_your_email: Inserisci il codice di verifica dalla tua e-mail. enter_the_verification_code_from_your_email: Inserisci il codice di verifica dalla tua e-mail.
too_many_attempts: Troppi tentativi. too_many_attempts: Troppi tentativi.
verification_email_bounced: "Impossibile inviare il codice di verifica: l'email a questo indirizzo è stata respinta."
verification_code: Codice di Verifica verification_code: Codice di Verifica
resend_code: Reinvia Codice resend_code: Reinvia Codice
verify_new_sign_in: Verifica nuovo accesso verify_new_sign_in: Verifica nuovo accesso
@@ -3231,6 +3269,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}'
@@ -3566,6 +3605,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
@@ -3693,6 +3733,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
@@ -3934,6 +3981,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é.
@@ -4121,6 +4171,7 @@ fr: &fr
the_code_has_been_sent_to_your_email: Le code a été envoyé à votre e‑mail. the_code_has_been_sent_to_your_email: Le code a été envoyé à votre e‑mail.
enter_the_verification_code_from_your_email: Saisissez le code de vérification reçu par e‑mail. enter_the_verification_code_from_your_email: Saisissez le code de vérification reçu par e‑mail.
too_many_attempts: Trop de tentatives. too_many_attempts: Trop de tentatives.
verification_email_bounced: "Impossible d'envoyer le code de vérification : l'e-mail à cette adresse a été rejeté."
verification_code: Code de vérification verification_code: Code de vérification
resend_code: Renvoyer le code resend_code: Renvoyer le code
verify_new_sign_in: Vérifier une nouvelle connexion verify_new_sign_in: Vérifier une nouvelle connexion
@@ -4335,6 +4386,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}"
@@ -4683,6 +4735,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
@@ -4810,6 +4863,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
@@ -5051,6 +5111,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.
@@ -5238,6 +5301,7 @@ pt: &pt
the_code_has_been_sent_to_your_email: O código foi enviado para seu e-mail. the_code_has_been_sent_to_your_email: O código foi enviado para seu e-mail.
enter_the_verification_code_from_your_email: Insira o código de verificação do seu e-mail. enter_the_verification_code_from_your_email: Insira o código de verificação do seu e-mail.
too_many_attempts: Muitas tentativas. too_many_attempts: Muitas tentativas.
verification_email_bounced: "Não foi possível enviar o código de verificação: o e-mail para este endereço não foi entregue."
verification_code: Código de Verificação verification_code: Código de Verificação
resend_code: Reenviar Código resend_code: Reenviar Código
verify_new_sign_in: Verificar novo login verify_new_sign_in: Verificar novo login
@@ -5455,6 +5519,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}'
@@ -5803,6 +5868,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
@@ -5930,6 +5996,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
@@ -6171,6 +6244,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.
@@ -6358,6 +6434,7 @@ de: &de
the_code_has_been_sent_to_your_email: Der Code wurde an Ihre E-Mail gesendet. the_code_has_been_sent_to_your_email: Der Code wurde an Ihre E-Mail gesendet.
enter_the_verification_code_from_your_email: Geben Sie den Verifizierungscode aus Ihrer E-Mail ein. enter_the_verification_code_from_your_email: Geben Sie den Verifizierungscode aus Ihrer E-Mail ein.
too_many_attempts: Zu viele Versuche. too_many_attempts: Zu viele Versuche.
verification_email_bounced: "Der Verifizierungscode konnte nicht gesendet werden: E-Mail an diese Adresse ist unzustellbar."
verification_code: Verifizierungscode verification_code: Verifizierungscode
resend_code: Code erneut senden resend_code: Code erneut senden
verify_new_sign_in: Neue Anmeldung verifizieren verify_new_sign_in: Neue Anmeldung verifizieren
@@ -6575,6 +6652,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}'
@@ -6790,11 +6868,14 @@ 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.
enter_the_verification_code_from_your_email: Wprowadź kod weryfikacyjny z Twojego e-maila. enter_the_verification_code_from_your_email: Wprowadź kod weryfikacyjny z Twojego e-maila.
too_many_attempts: Zbyt wiele prób. too_many_attempts: Zbyt wiele prób.
verification_email_bounced: "Nie można wysłać kodu weryfikacyjnego: e-mail na ten adres został odrzucony."
back: Wstecz
verification_code: Kod Weryfikacyjny verification_code: Kod Weryfikacyjny
resend_code: Wyślij Kod Ponownie resend_code: Wyślij Kod Ponownie
powered_by: 'Napędzany przez' powered_by: 'Napędzany przez'
@@ -6929,11 +7010,14 @@ 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: Код було надіслано на вашу електронну пошту.
enter_the_verification_code_from_your_email: Введіть код перевірки з вашої електронної пошти. enter_the_verification_code_from_your_email: Введіть код перевірки з вашої електронної пошти.
too_many_attempts: Забагато спроб. too_many_attempts: Забагато спроб.
verification_email_bounced: "Не вдалося надіслати код підтвердження: лист на цю адресу було відхилено."
back: Назад
verification_code: Код перевірки verification_code: Код перевірки
resend_code: Надіслати код знову resend_code: Надіслати код знову
powered_by: 'Працює на базі' powered_by: 'Працює на базі'
@@ -7068,11 +7152,14 @@ 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.
enter_the_verification_code_from_your_email: Zadejte ověřovací kód z vašeho e-mailu. enter_the_verification_code_from_your_email: Zadejte ověřovací kód z vašeho e-mailu.
too_many_attempts: Příliš mnoho pokusů. too_many_attempts: Příliš mnoho pokusů.
verification_email_bounced: "Ověřovací kód nelze odeslat: e-mail na tuto adresu byl odmítnut."
back: Zpět
verification_code: Ověřovací kód verification_code: Ověřovací kód
resend_code: Znovu odeslat kód resend_code: Znovu odeslat kód
powered_by: 'Poháněno' powered_by: 'Poháněno'
@@ -7193,11 +7280,14 @@ 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: הקוד נשלח לדוא"ל שלך.
enter_the_verification_code_from_your_email: הזן את קוד האימות מדוא"ל שלך. enter_the_verification_code_from_your_email: הזן את קוד האימות מדוא"ל שלך.
too_many_attempts: יותר מדי ניסיונות. too_many_attempts: יותר מדי ניסיונות.
verification_email_bounced: "לא ניתן לשלוח את קוד האימות: האימייל לכתובת זו נדחה."
back: חזרה
verification_code: קוד אימות verification_code: קוד אימות
resend_code: שלח קוד מחדש resend_code: שלח קוד מחדש
powered_by: 'מופעל על ידי' powered_by: 'מופעל על ידי'
@@ -7439,6 +7529,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
@@ -7566,6 +7657,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
@@ -7807,6 +7905,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.
@@ -7933,7 +8034,7 @@ nl: &nl
awaiting: Wachten awaiting: Wachten
document_id: Document-ID document_id: Document-ID
envelope_id: Envelop-ID envelope_id: Envelop-ID
event_log: Gebeurtenissenlogboek event_log: Gebeurtenislogboek
verify: Verifiëren verify: Verifiëren
testing_log_not_for_production_use: Testlogboek - Niet voor productiegebruik testing_log_not_for_production_use: Testlogboek - Niet voor productiegebruik
original_sha256: Oorspronkelijke SHA256 original_sha256: Oorspronkelijke SHA256
@@ -7994,6 +8095,7 @@ nl: &nl
the_code_has_been_sent_to_your_email: De code is naar uw e-mail verzonden. the_code_has_been_sent_to_your_email: De code is naar uw e-mail verzonden.
enter_the_verification_code_from_your_email: Voer de verificatiecode uit uw e-mail in. enter_the_verification_code_from_your_email: Voer de verificatiecode uit uw e-mail in.
too_many_attempts: Te veel pogingen. too_many_attempts: Te veel pogingen.
verification_email_bounced: "Verificatiecode kan niet worden verzonden: e-mail naar dit adres is geretourneerd."
verification_code: Verificatiecode verification_code: Verificatiecode
resend_code: Code opnieuw verzenden resend_code: Code opnieuw verzenden
verify_new_sign_in: Nieuwe aanmelding verifiëren verify_new_sign_in: Nieuwe aanmelding verifiëren
@@ -8207,6 +8309,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}"
@@ -8409,11 +8512,14 @@ 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: تم إرسال الرمز إلى بريدك الإلكتروني.
enter_the_verification_code_from_your_email: أدخل رمز التحقق من بريدك الإلكتروني. enter_the_verification_code_from_your_email: أدخل رمز التحقق من بريدك الإلكتروني.
too_many_attempts: عدد المحاولات كبير جدًا. too_many_attempts: عدد المحاولات كبير جدًا.
verification_email_bounced: "تعذر إرسال رمز التحقق: تم رفض البريد الإلكتروني المرسل إلى هذا العنوان."
back: رجوع
verification_code: رمز التحقق verification_code: رمز التحقق
resend_code: إعادة إرسال الرمز resend_code: إعادة إرسال الرمز
powered_by: 'مشغل بواسطة' powered_by: 'مشغل بواسطة'
@@ -8520,11 +8626,14 @@ 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: 코드가 이메일로 전송되었습니다.
enter_the_verification_code_from_your_email: 이메일로 받은 인증 코드를 입력하세요. enter_the_verification_code_from_your_email: 이메일로 받은 인증 코드를 입력하세요.
too_many_attempts: 시도 횟수가 너무 많습니다. too_many_attempts: 시도 횟수가 너무 많습니다.
verification_email_bounced: "인증 코드를 보낼 수 없습니다: 이 주소로 보낸 이메일이 반송되었습니다."
back: 뒤로
verification_code: 인증 코드 verification_code: 인증 코드
resend_code: 코드 재전송 resend_code: 코드 재전송
powered_by: '제공:' powered_by: '제공:'
@@ -8631,11 +8740,14 @@ 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: コードがあなたのメールに送信されました
enter_the_verification_code_from_your_email: メールに記載された認証コードを入力してください enter_the_verification_code_from_your_email: メールに記載された認証コードを入力してください
too_many_attempts: 試行回数が多すぎます too_many_attempts: 試行回数が多すぎます
verification_email_bounced: "認証コードを送信できません:このアドレス宛のメールが配信できませんでした。"
back: 戻る
verification_code: 認証コード verification_code: 認証コード
resend_code: コードを再送信 resend_code: コードを再送信
powered_by: '提供元:' powered_by: '提供元:'
+6 -2
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
@@ -174,10 +175,13 @@ Rails.application.routes.draw do
resources :submitters, only: %i[] do resources :submitters, only: %i[] do
resources :download, only: %i[index], controller: 'submitters_download', constraints: { submitter_id: /\d+/ } resources :download, only: %i[index], controller: 'submitters_download', constraints: { submitter_id: /\d+/ }
resources :download, only: %i[index], controller: 'submit_form_completed_download'
resources :send_email, only: %i[create], controller: 'submitters_send_email' resources :send_email, only: %i[create], controller: 'submitters_send_email'
end end
resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submit_form_completed_download'
end
resources :settings, only: %i[index] resources :settings, only: %i[index]
scope '/settings', as: :settings do scope '/settings', as: :settings do
-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"] %>
@@ -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"
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
module FilenameUtils
DANGEROUS_EXTENSIONS = %w[
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
hta cpl jar app deb rpm dmg pkg mpkg dll so dylib sys
inf reg ps1 psm1 psd1 ps1xml psc1 pssc vb vba
sh bash zsh fish run out bin elf gadget workflow lnk scf
url desktop application action apk ipa xap appx
appxbundle msix msixbundle diagcab diagpkg msc ocx
drv ins isp mst paf prf shb shs slk ws wsc inf1 inf2
].freeze
DANGEROUS_EXTENSIONS_REGEXP = /\.(#{Regexp.union(DANGEROUS_EXTENSIONS).source})\W*\z/i
module_function
def dangerous_extension(filename)
ActiveStorage::Filename.wrap(filename).sanitized[DANGEROUS_EXTENSIONS_REGEXP, 1]&.downcase
end
end
+18 -8
View File
@@ -1,23 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
module ImageUtils module ImageUtils
PNG_REGEXP = %r{\Aimage/(?:png|apng|vnd\.mozilla\.apng)\z}
JPEG_REGEXP = %r{\Aimage/(?:jpeg|jpg|pjpeg)\z}
ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z} ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z}
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z} BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
UnsupportedFormat = Class.new(StandardError)
module_function module_function
def load_vips(data, content_type: nil, autorot: false) def load_vips(data, content_type: nil, autorot: false)
content_type ||= Marcel::MimeType.for(data) content_type ||= Marcel::MimeType.for(data)
if ICO_REGEXP.match?(content_type) image =
LoadIco.call(data) case content_type
elsif BMP_REGEXP.match?(content_type) when PNG_REGEXP
LoadBmp.call(data) Vips::Image.pngload_buffer(data)
else when JPEG_REGEXP
image = Vips::Image.new_from_buffer(data, '') Vips::Image.jpegload_buffer(data)
when ICO_REGEXP
LoadIco.call(data)
when BMP_REGEXP
LoadBmp.call(data)
else
raise UnsupportedFormat, content_type.to_s
end
autorot ? image.autorot : image autorot ? image.autorot : image
end
end end
def blank?(image) def blank?(image)
+34 -78
View File
@@ -3,6 +3,9 @@
module LoadBmp module LoadBmp
BPPS = [1, 4, 8, 24, 32].freeze BPPS = [1, 4, 8, 24, 32].freeze
MAX_COORD = ENV.fetch('VIPS_MAX_COORD', '17000').to_i
MAX_PIXELS = 145_000_000
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
@@ -18,40 +21,28 @@ module LoadBmp
header_data[:height] header_data[:height]
) )
if header_data[:bpp] <= 8 padded_rows = Vips::Image.new_from_memory_copy(
final_pixel_data = decode_indexed_pixel_data( raw_pixel_data_from_file,
raw_pixel_data_from_file, header_data[:bmp_stride],
header_data[:bpp], header_data[:height],
header_data[:width], 1,
header_data[:height], :uchar
header_data[:bmp_stride], )
header_data[:color_table]
)
bands = 3
else
final_pixel_data = prepare_unpadded_pixel_data_string(
raw_pixel_data_from_file,
header_data[:bpp],
header_data[:width],
header_data[:height],
header_data[:bmp_stride]
)
bands = header_data[:bpp] / 8
end
image = Vips::Image.new_from_memory_copy(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
image = image.flip(:vertical) if header_data[:orientation] == -1
image_rgb = image_rgb =
if header_data[:bpp] <= 8 if header_data[:bpp] <= 8
image decode_indexed_pixel_data(padded_rows, header_data[:bpp], header_data[:width], header_data[:color_table])
elsif bands == 3 else
image.recomb(band3_recomb) bands = header_data[:bpp] / 8
elsif bands == 4
image.recomb(band4_recomb) image = padded_rows.extract_area(0, 0, header_data[:width] * bands, header_data[:height])
.bandfold(factor: bands)
bands == 3 ? image.recomb(band3_recomb) : image.recomb(band4_recomb)
end end
image_rgb = image_rgb.flip(:vertical) if header_data[:orientation] == -1
image_rgb = image_rgb.copy(interpretation: :srgb) if image_rgb.interpretation != :srgb image_rgb = image_rgb.copy(interpretation: :srgb) if image_rgb.interpretation != :srgb
image_rgb image_rgb
@@ -98,6 +89,9 @@ module LoadBmp
raise ArgumentError, 'BMP width must be positive.' if width <= 0 raise ArgumentError, 'BMP width must be positive.' if width <= 0
raise ArgumentError, 'BMP height must be positive.' if height <= 0 raise ArgumentError, 'BMP height must be positive.' if height <= 0
if width > MAX_COORD || height > MAX_COORD || width * height > MAX_PIXELS
raise ArgumentError, "BMP dimensions are too large: #{width}x#{height}."
end
if compression != 0 if compression != 0
raise ArgumentError, raise ArgumentError,
@@ -164,62 +158,24 @@ module LoadBmp
raw_pixel_data_from_file raw_pixel_data_from_file
end end
def prepare_unpadded_pixel_data_string(raw_pixel_data_from_file, bpp, width, height, bmp_stride) def decode_indexed_pixel_data(padded_rows, bpp, width, color_table)
bytes_per_pixel = bpp / 8 pixels_per_byte = 8 / bpp
actual_row_width_bytes = width * bytes_per_pixel
unpadded_rows = Array.new(height) image = padded_rows.maplut(build_palette_lut(bpp, color_table))
current_offset_in_blob = 0 image = image.bandunfold.bandfold(factor: 3) if pixels_per_byte > 1
height.times do |i| image.extract_area(0, 0, width, padded_rows.height)
if current_offset_in_blob + actual_row_width_bytes > raw_pixel_data_from_file.bytesize
raise ArgumentError,
"Not enough data in pixel blob for row #{i}. Offset #{current_offset_in_blob}, " \
"row width #{actual_row_width_bytes}, blob size #{raw_pixel_data_from_file.bytesize}"
end
unpadded_row_slice = raw_pixel_data_from_file.byteslice(current_offset_in_blob, actual_row_width_bytes)
if unpadded_row_slice.nil? || unpadded_row_slice.bytesize < actual_row_width_bytes
raise ArgumentError, "Failed to slice a full unpadded row from pixel data blob for row #{i}."
end
unpadded_rows[i] = unpadded_row_slice
current_offset_in_blob += bmp_stride
end
unpadded_rows.join
end end
def decode_indexed_pixel_data(raw_data, bpp, width, height, bmp_stride, color_table) def build_palette_lut(bpp, color_table)
palette = color_table.map { |r, g, b| [r, g, b].pack('CCC') } pixels_per_byte = 8 / bpp
mask = (1 << bpp) - 1
output = String.new(capacity: width * height * 3) entries = Array.new(256) do |byte_val|
(0...pixels_per_byte).flat_map { |i| color_table[(byte_val >> ((pixels_per_byte - 1 - i) * bpp)) & mask] }
height.times do |y|
row_offset = y * bmp_stride
case bpp
when 1
width.times do |x|
byte_val = raw_data.getbyte(row_offset + (x >> 3))
index = (byte_val >> (7 - (x & 7))) & 0x01
output << palette[index]
end
when 4
width.times do |x|
byte_val = raw_data.getbyte(row_offset + (x >> 1))
index = x.even? ? (byte_val >> 4) & 0x0F : byte_val & 0x0F
output << palette[index]
end
when 8
width.times do |x|
output << palette[raw_data.getbyte(row_offset + x)]
end
end
end end
output Vips::Image.new_from_memory_copy(entries.flatten.pack('C*'), 256, 1, pixels_per_byte * 3, :uchar)
end end
def band3_recomb def band3_recomb
+1 -1
View File
@@ -94,7 +94,7 @@ module LoadIco
palette = [] palette = []
if dib_bpp <= 8 if dib_bpp <= 8
num_palette_entries = dib_clr_used.zero? ? (1 << dib_bpp) : dib_clr_used num_palette_entries = [dib_clr_used.zero? ? (1 << dib_bpp) : dib_clr_used, 1 << dib_bpp].min
num_palette_entries.times do num_palette_entries.times do
palette_color_bytes = dib_io.read(4) palette_color_bytes = dib_io.read(4)
return nil unless palette_color_bytes && palette_color_bytes.bytesize == 4 return nil unless palette_color_bytes && palette_color_bytes.bytesize == 4
@@ -29,8 +29,6 @@ module Params
in_path(params, :message) do |message_params| in_path(params, :message) do |message_params|
type(message_params, :subject, String) type(message_params, :subject, String)
type(message_params, :body, String) type(message_params, :body, String)
required(message_params, :body)
end end
end end
@@ -53,8 +51,6 @@ module Params
in_path(params, :message, skip_blank: true) do |message_params| in_path(params, :message, skip_blank: true) do |message_params|
type(message_params, :subject, String) type(message_params, :subject, String)
type(message_params, :body, String) type(message_params, :body, String)
required(message_params, :body)
end end
value_in(params, :order, %w[preserved random], allow_nil: true) value_in(params, :order, %w[preserved random], allow_nil: true)
@@ -119,8 +115,6 @@ module Params
in_path(params, :message) do |message_params| in_path(params, :message) do |message_params|
type(message_params, :subject, String) type(message_params, :subject, String)
type(message_params, :body, String) type(message_params, :body, String)
required(message_params, :body)
end end
value_in(params, :order, %w[preserved random], allow_nil: true) value_in(params, :order, %w[preserved random], allow_nil: true)
+12 -36
View File
@@ -6,43 +6,19 @@ module PdfUtils
module_function module_function
def encrypted?(data, password: nil) def merge(io_files)
HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
false
rescue HexaPDF::EncryptionError
true
end
def decrypt(data, password)
encrypted_doc = HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
decrypted_doc = HexaPDF::Document.new
encrypted_doc.pages.each do |page|
decrypted_doc.pages << decrypted_doc.import(page)
end
decrypted_io = StringIO.new
decrypted_doc.write(decrypted_io, validate: false)
decrypted_io.tap(&:rewind).read
end
def merge(files)
merged_pdf = HexaPDF::Document.new
files.each do |file|
pdf = HexaPDF::Document.new(io: file)
pdf.pages.each { |page| merged_pdf.pages << merged_pdf.import(page) }
end
merged_content = StringIO.new merged_content = StringIO.new
merged_pdf.validate(auto_correct: true)
merged_pdf.write(merged_content, validate: false)
merged_content.rewind
merged_content Pdfium.with_instance do
Pdfium::Document.create do |merged_pdf|
io_files.each do |io|
Pdfium::Document.open_io(io) { |pdf| merged_pdf.import_pages(pdf) }
end
merged_pdf.save(merged_content)
end
end
merged_content.tap(&:rewind)
end end
end end
+894 -18
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -54,6 +54,7 @@ Puma::Plugin.create do
Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty? Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty?
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"), exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
'--bind', '127.0.0.1', '--port', '16379',
'--loglevel', 'warning') '--loglevel', 'warning')
end end
end end
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more