Compare commits

...
87 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
118 changed files with 1189 additions and 978 deletions

No files matched your search

+1 -5
View File
@@ -152,11 +152,7 @@ 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
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip" wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-musl-$(uname -m).zip"
case "$(uname -m)" in
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;;
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;;
esac | sha256sum -c -
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
rm -rf pdfium-linux.zip /tmp/pdfium-linux rm -rf pdfium-linux.zip /tmp/pdfium-linux
+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/20260813/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 "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;; \ x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 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'
+5 -4
View File
@@ -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
@@ -63,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,
+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
+1 -1
View File
@@ -133,7 +133,7 @@ class ProcessSubmitterCompletionJob
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 || !is_copy_email_enabled) && (!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!
+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 -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>
+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]'}" %>
+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)
+78 -1
View File
@@ -790,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
@@ -992,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.
@@ -1007,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}'
@@ -1470,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
@@ -1901,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
@@ -2118,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}'
@@ -2595,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
@@ -3026,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
@@ -3243,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}'
@@ -3706,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
@@ -4137,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
@@ -4351,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}"
@@ -4827,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
@@ -5258,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
@@ -5475,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}'
@@ -5951,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
@@ -6382,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
@@ -6599,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}'
@@ -6820,6 +6874,8 @@ pl:
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'
@@ -6960,6 +7016,8 @@ uk:
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: 'Працює на базі'
@@ -7100,6 +7158,8 @@ cs:
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'
@@ -7226,6 +7286,8 @@ he:
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: 'מופעל על ידי'
@@ -7595,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
@@ -7965,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
@@ -8026,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
@@ -8239,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}"
@@ -8447,6 +8518,8 @@ ar:
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: 'مشغل بواسطة'
@@ -8559,6 +8632,8 @@ ko:
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: '제공:'
@@ -8671,6 +8746,8 @@ ja:
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)
-18
View File
@@ -6,24 +6,6 @@ module PdfUtils
module_function module_function
def encrypted?(data, password: nil)
HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
false
rescue HexaPDF::EncryptionError
true
end
def decrypt(data, password)
decrypted_io = StringIO.new
Pdfium::Document.open_bytes(data, password) do |doc|
doc.save(decrypted_io, flags: Pdfium::FPDF_REMOVE_SECURITY)
end
decrypted_io.tap(&:rewind).read
end
def merge(io_files) def merge(io_files)
merged_content = StringIO.new merged_content = StringIO.new
+40 -2
View File
@@ -105,9 +105,11 @@ class Pdfium
attach_function :FPDF_LoadCustomDocument, %i[pointer FPDF_STRING], :FPDF_DOCUMENT attach_function :FPDF_LoadCustomDocument, %i[pointer FPDF_STRING], :FPDF_DOCUMENT
attach_function :FPDF_CloseDocument, [:FPDF_DOCUMENT], :void attach_function :FPDF_CloseDocument, [:FPDF_DOCUMENT], :void
attach_function :FPDF_GetPageCount, [:FPDF_DOCUMENT], :int attach_function :FPDF_GetPageCount, [:FPDF_DOCUMENT], :int
attach_function :FPDF_GetPageSizeByIndexF, %i[FPDF_DOCUMENT int pointer], :int
attach_function :FPDF_GetLastError, [], :ulong attach_function :FPDF_GetLastError, [], :ulong
attach_function :FPDF_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong attach_function :FPDF_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong
attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int
attach_function :FPDF_HasOnlyDSSChanges, %i[FPDF_DOCUMENT FPDF_DOCUMENT], :int
attach_function :FPDF_GetSecurityHandlerRevision, [:FPDF_DOCUMENT], :int attach_function :FPDF_GetSecurityHandlerRevision, [:FPDF_DOCUMENT], :int
attach_function :FPDF_GetFormType, [:FPDF_DOCUMENT], :int attach_function :FPDF_GetFormType, [:FPDF_DOCUMENT], :int
@@ -282,6 +284,7 @@ class Pdfium
attach_function :FPDFPage_GetAnnotCount, [:FPDF_PAGE], :int attach_function :FPDFPage_GetAnnotCount, [:FPDF_PAGE], :int
attach_function :FPDFPage_GetAnnotCountRaw, %i[FPDF_DOCUMENT int], :int attach_function :FPDFPage_GetAnnotCountRaw, %i[FPDF_DOCUMENT int], :int
attach_function :FPDFPage_GetRotationRaw, %i[FPDF_DOCUMENT int], :int
attach_function :FPDFPage_GetAnnot, %i[FPDF_PAGE int], :FPDF_ANNOTATION attach_function :FPDFPage_GetAnnot, %i[FPDF_PAGE int], :FPDF_ANNOTATION
attach_function :FPDFPage_CloseAnnot, [:FPDF_ANNOTATION], :void attach_function :FPDFPage_CloseAnnot, [:FPDF_ANNOTATION], :void
attach_function :FPDFAnnot_GetSubtype, [:FPDF_ANNOTATION], :int attach_function :FPDFAnnot_GetSubtype, [:FPDF_ANNOTATION], :int
@@ -368,6 +371,11 @@ class Pdfium
:bottom, :float :bottom, :float
end end
class FS_SIZEF < FFI::Struct
layout :width, :float,
:height, :float
end
class FS_MATRIX < FFI::Struct class FS_MATRIX < FFI::Struct
layout :a, :float, layout :a, :float,
:b, :float, :b, :float,
@@ -531,6 +539,8 @@ class Pdfium
@pages = {} @pages = {}
@annot_counts = {} @annot_counts = {}
@page_sizes = {}
@page_rotations = {}
@closed = false @closed = false
@source_buffer = source_buffer @source_buffer = source_buffer
@form_handle = FFI::Pointer::NULL @form_handle = FFI::Pointer::NULL
@@ -555,6 +565,27 @@ class Pdfium
@page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr) @page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr)
end end
def page_size(page_index)
@page_sizes[page_index] ||=
begin
size = Pdfium::FS_SIZEF.new
[size[:width], size[:height]] if Pdfium.FPDF_GetPageSizeByIndexF(@document_ptr, page_index, size) == 1
end
end
def reset_page_size(page_index)
@page_sizes.delete(page_index)
end
def page_rotation(page_index)
@page_rotations[page_index] ||= Pdfium.FPDFPage_GetRotationRaw(@document_ptr, page_index)
end
def reset_page_rotation(page_index)
@page_rotations.delete(page_index)
end
def encrypted? def encrypted?
Pdfium.FPDF_GetSecurityHandlerRevision(@document_ptr) >= 0 Pdfium.FPDF_GetSecurityHandlerRevision(@document_ptr) >= 0
end end
@@ -745,6 +776,10 @@ class Pdfium
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1 Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
end end
def only_dss_changes?(signed_document)
Pdfium.FPDF_HasOnlyDSSChanges(@document_ptr, signed_document.document_ptr) == 1
end
def annot_count(page_index) def annot_count(page_index)
@annot_counts[page_index] ||= Pdfium.FPDFPage_GetAnnotCountRaw(@document_ptr, page_index) @annot_counts[page_index] ||= Pdfium.FPDFPage_GetAnnotCountRaw(@document_ptr, page_index)
end end
@@ -983,6 +1018,9 @@ class Pdfium
def rotation=(value) def rotation=(value)
Pdfium.FPDFPage_SetRotation(@page_ptr, value) Pdfium.FPDFPage_SetRotation(@page_ptr, value)
@document.reset_page_size(@page_index)
@document.reset_page_rotation(@page_index)
@rotation = value @rotation = value
end end
@@ -2278,14 +2316,14 @@ class Pdfium
[PageObject, *to_a].hash [PageObject, *to_a].hash
end end
private
def object_ptr def object_ptr
page.ensure_not_closed! page.ensure_not_closed!
Pdfium.FPDFPage_GetObject(page.page_ptr, index) Pdfium.FPDFPage_GetObject(page.page_ptr, index)
end end
private
def read_bounds def read_bounds
buffer = Array.new(4) { FFI::MemoryPointer.new(:float) } buffer = Array.new(4) { FFI::MemoryPointer.new(:float) }
+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
+2
View File
@@ -236,6 +236,8 @@ module SearchEntries
end end
def add_hyphens(entry, text) def add_hyphens(entry, text)
text = text.tr('\\', ' ')
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/) hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
hyphens.uniq.each_with_index do |item, index| hyphens.uniq.each_with_index do |item, index|
+14 -3
View File
@@ -3,6 +3,11 @@
module Submissions module Submissions
DEFAULT_SUBMITTERS_ORDER = 'random' DEFAULT_SUBMITTERS_ORDER = 'random'
SKIP_EMAIL_FIX_LABELS = %w[ga gami gasi gil gma gmao gmi gmsi goi ol].freeze
SKIP_EMAIL_FIX_TLDS =
/\.(?:gob(?:\.\w+)?|om|mm|cm|et|mo|nz|za|ie|mom|free|comsec|unicom|ed(?:\.\w+){1,2})\z/i
module_function module_function
def maybe_update_completed_at(submission) def maybe_update_completed_at(submission)
@@ -210,7 +215,7 @@ module Submissions
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i) return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
return email.downcase if email.include?(',') || return email.downcase if email.include?(',') ||
email.match?(/\.(?:gob(?:\.\w+)?|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) || email.match?(SKIP_EMAIL_FIX_TLDS) ||
email.exclude?('.') email.exclude?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<')) fixed_email = EmailTypo.call(email.delete_prefix('<'))
@@ -220,8 +225,7 @@ module Submissions
domain = email.split('@').last.to_s.downcase domain = email.split('@').last.to_s.downcase
fixed_domain = fixed_email.to_s.split('@').last fixed_domain = fixed_email.to_s.split('@').last
return email.downcase if domain == fixed_domain return email.downcase if domain == fixed_domain || skip_email_fix?(domain, fixed_domain)
return email.downcase if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
threshold = fixed_domain.start_with?('hotmail.') ? 2 : 3 threshold = fixed_domain.start_with?('hotmail.') ? 2 : 3
@@ -236,6 +240,13 @@ module Submissions
fixed_email fixed_email
end end
def skip_email_fix?(domain, fixed_domain)
return true if SKIP_EMAIL_FIX_LABELS.include?(domain.split('.').first)
return true if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
fixed_domain.match?(/\A(?:comcast|verizon)\./i) && !domain.match?(/\A(?:comcast|verizon)\./i)
end
def filtered_conditions_schema(submission, values: nil, include_submitter_uuid: nil) def filtered_conditions_schema(submission, values: nil, include_submitter_uuid: nil)
(submission.template_schema || submission.template.schema).filter_map do |item| (submission.template_schema || submission.template.schema).filter_map do |item|
if item['conditions'].present? if item['conditions'].present?
+20 -10
View File
@@ -19,6 +19,7 @@ module Submissions
}.freeze }.freeze
TESTING_FOOTER = GenerateResultAttachments::TESTING_FOOTER TESTING_FOOTER = GenerateResultAttachments::TESTING_FOOTER
UNSUPPORTED_IMAGE_TYPES = GenerateResultAttachments::UNSUPPORTED_IMAGE_TYPES
RTL_REGEXP = TextUtils::RTL_REGEXP RTL_REGEXP = TextUtils::RTL_REGEXP
MAX_IMAGE_HEIGHT = 100 MAX_IMAGE_HEIGHT = 100
@@ -61,7 +62,7 @@ module Submissions
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - " \ io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - " \
"#{submission.name || submission.template&.name}.pdf" "#{submission.name || submission.template&.name}.pdf".tr('/', '-')
), ),
name: 'audit_trail', name: 'audit_trail',
record: submission record: submission
@@ -219,7 +220,7 @@ module Submissions
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[{ text: document.filename.to_s }] [{ text: TextUtils.maybe_rtl_reverse(document.filename.to_s) }]
), ),
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
@@ -272,7 +273,9 @@ module Submissions
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
submission.template_submitters.size > 1 && { text: "#{item['name']}\n" }, submission.template_submitters.size > 1 && {
text: "#{TextUtils.maybe_rtl_reverse(item['name'].to_s)}\n"
},
submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] }, submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] },
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" }, submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
submitter.phone && { text: "#{submitter.phone}\n" } submitter.phone && { text: "#{submitter.phone}\n" }
@@ -299,7 +302,7 @@ module Submissions
}, },
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" }, completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" }, completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" }, completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua'].to_s.squish}\n" },
submitter.timezone && { text: "Time zone: #{submitter.timezone.to_s.sub('Kiev', 'Kyiv')}\n" }, submitter.timezone && { text: "Time zone: #{submitter.timezone.to_s.sub('Kiev', 'Kyiv')}\n" },
"\n" "\n"
].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0] ].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0]
@@ -352,8 +355,9 @@ module Submissions
field_type = field['type'] field_type = field['type']
if field_type == 'image' && if (field_type == 'image' || field_type == 'stamp') &&
submitter.attachments.find { |a| a.uuid == value }.then { |a| !a.image? || a.content_type == 'image/heic' } submitter.attachments.find { |a| a.uuid == value }
.then { |a| !a.image? || a.content_type.in?(UNSUPPORTED_IMAGE_TYPES) }
field_type = 'file' field_type = 'file'
end end
@@ -425,7 +429,7 @@ module Submissions
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options) r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
end end
{ link:, text: "#{attachment.filename}\n", style: :link } { link:, text: "#{TextUtils.maybe_rtl_reverse(attachment.filename.to_s)}\n", style: :link }
end, end,
padding: [0, 0, 10, 0] padding: [0, 0, 10, 0]
) )
@@ -476,6 +480,8 @@ module Submissions
submitter.name || submitter.email || submitter.phone submitter.name || submitter.email || submitter.phone
end end
submitter_name = TextUtils.maybe_rtl_reverse(submitter_name.to_s.squish)
text = text =
if event.event_type == 'complete_verification' if event.event_type == 'complete_verification'
I18n.t('submission_event_names.complete_verification_by_html', provider: event.data['method'], I18n.t('submission_event_names.complete_verification_by_html', provider: event.data['method'],
@@ -483,18 +489,22 @@ module Submissions
elsif event.event_type == 'invite_party' && elsif event.event_type == 'invite_party' &&
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) (name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
invited_submitter_name = [invited_submitter.name || invited_submitter.email || invited_submitter.phone, invited_submitter_name = TextUtils.maybe_rtl_reverse(
name].join(' ') [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' ').squish
)
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:, I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
submitter_name:) submitter_name:)
elsif with_audit_sender && (event.event_type == 'send_email' || event.event_type == 'send_sms') elsif with_audit_sender && (event.event_type == 'send_email' || event.event_type == 'send_sms')
created_by_name = TextUtils.maybe_rtl_reverse(submission.created_by_user.full_name.to_s.squish)
[ [
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:), I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:),
"<b>#{I18n.t(:from)}</b> #{submission.created_by_user.full_name} #{submission.created_by_user.email}" "<b>#{I18n.t(:from)}</b> #{created_by_name} #{submission.created_by_user.email}"
].join("\n") ].join("\n")
elsif event.event_type == 'delegate_form' elsif event.event_type == 'delegate_form'
from = event.data['old_email'].presence || from = event.data['old_email'].presence ||
versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone } versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }
from = TextUtils.maybe_rtl_reverse(from.to_s.squish)
I18n.t('submission_event_names.delegate_form_by_html', from:, to: event.data['email']) I18n.t('submission_event_names.delegate_form_by_html', from:, to: event.data['email'])
elsif event.event_type.include?('send_') elsif event.event_type.include?('send_')
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:) I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
@@ -37,7 +37,7 @@ module Submissions
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{submission.name || submission.template&.name}.pdf" io: io.tap(&:rewind), filename: "#{submission.name || submission.template&.name}.pdf".tr('/', '-')
), ),
name: with_audit ? 'combined_document' : 'merged_document', name: with_audit ? 'combined_document' : 'merged_document',
record: submission record: submission
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more