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 install --jobs 4 --retry 4
yarn install
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/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 -
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-musl-$(uname -m).zip"
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
rm -rf pdfium-linux.zip /tmp/pdfium-linux
+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/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 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 \
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;; \
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;; \
x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
aarch64) echo "73b72ed79b9bfdd494dee7e863482c24971a8d4b11f6cd1289aa170d31304a1f pdfium-linux.zip" ;; \
esac | sha256sum -c - && \
mkdir -p /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 'rqrcode', require: false
gem 'ruby-vips'
gem 'rubyXL', require: false
gem 'rubyzip', require: false
gem 'shakapacker'
gem 'sidekiq'
gem 'sqlite3', require: false
@@ -45,6 +45,7 @@ gem 'trilogy', require: false
gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data'
gem 'xlsxtream', require: false
group :development, :test do
gem 'better_html'
+5 -4
View File
@@ -497,9 +497,6 @@ GEM
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
rubyXL (3.4.35)
nokogiri (>= 1.10.8)
rubyzip (>= 3.2.2)
rubyzip (3.2.2)
securerandom (0.4.1)
semantic_range (3.1.1)
@@ -575,10 +572,13 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7)
xpath (3.2.0)
nokogiri (~> 1.8)
yaml (0.4.0)
zeitwerk (2.8.2)
zip_kit (6.3.4)
PLATFORMS
aarch64-linux
@@ -638,7 +638,7 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
ruby-vips
rubyXL
rubyzip
shakapacker
sidekiq
simplecov
@@ -650,6 +650,7 @@ DEPENDENCIES
tzinfo-data
web-console
webmock
xlsxtream
RUBY VERSION
ruby 4.0.5
+13 -9
View File
@@ -23,17 +23,20 @@ class AccountsController < ApplicationController
unless Docuseal.multitenant?
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
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])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
if can?(:manage, @encrypted_config)
@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
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end
with_locale do
@@ -47,7 +50,8 @@ class AccountsController < ApplicationController
authorize!(:manage, current_account)
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)
# rubocop:disable Layout/LineLength
@@ -23,7 +23,7 @@ module Api
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)
return head :unprocessable_content
@@ -19,7 +19,7 @@ module Api
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)
return head :unprocessable_content
+6 -7
View File
@@ -17,24 +17,23 @@ module Api
def verify
file = Base64.decode64(params[:file])
pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
render json: {
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,
signer_name: sig.signer_name,
signing_reason: sig.signing_reason,
verification_result: sig.messages.map { |m| { type: m.status || :info, content: m.text } },
signer_name: sig.common_name,
signing_reason: sig.reason,
signing_time: sig.signing_time,
signature_type: sig.signature_type
signature_type: sig.type
}
end
}
rescue HexaPDF::MalformedPDFError
rescue Pdfium::PdfiumError
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end
end
+1 -1
View File
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
rescue_from RateLimit::LimitApproached do |e|
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
if Rails.env.production? || Rails.env.test?
@@ -46,6 +46,8 @@ module Mcp
authorize!(:create, @template)
Templates.maybe_assign_access(@template)
if mcp_params['url'].present?
tempfile = Tempfile.new
tempfile.binmode
+4 -4
View File
@@ -14,14 +14,14 @@ class MfaSetupController < ApplicationController
def edit; end
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])
current_user.otp_required_for_login = true
current_user.save!
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
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)
@error_message = I18n.t('code_is_invalid')
@@ -31,13 +31,13 @@ class MfaSetupController < ApplicationController
end
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])
current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
else
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
@error_message = I18n.t('code_is_invalid')
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
preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
preview_image =
attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id].to_i}.png", "#{params[:id].to_i}.jpg"] })
if preview_image
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]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update
before_action :load_template
before_action :authorize_start!, only: :update
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
raise ActionController::RoutingError, I18n.t('not_found')
@@ -23,8 +19,7 @@ class StartFormController < ApplicationController
if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
uuid: Submitters::StartForm.first_submitter_uuid(@template))
render :email_verification if params[:email_verification]
else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -41,24 +36,28 @@ class StartFormController < ApplicationController
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
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
return render :show, status: :unprocessable_content
end
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)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter, is_new_record:)
if require_link_2fa?(@template, @submitter)
handle_require_2fa(@template, @submitter)
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)
else
@@ -88,41 +87,40 @@ class StartFormController < ApplicationController
private
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
def find_or_initialize_submitter(template, submitter_params)
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
return unless expire_at
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id,
'expire_at' => expire_at.to_i)
end
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)
submitter =
if blank_fields.present?
Submitter.new(submitter_params)
else
Submitters::StartForm.find_or_initialize_submitter(
template, submitter_params, exclude_completed: params[:resubmit].present?,
request:, current_user:
)
end
blank_fields.each { |key| submitter.errors.add(key.to_sym, :blank) }
submitter
end
def can_resubmit?(submitter)
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
def require_link_2fa?(template, submitter)
return true if template.preferences['shared_link_2fa'] == true
return false if cookies.encrypted[:start_form_slug] == submitter.slug
return false if current_user && submitter.email == current_user.email &&
current_user.account_id == submitter.account_id
!submitter.new_record?
end
def authorize_start!
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 if @resubmit_submitter
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -130,90 +128,14 @@ class StartFormController < ApplicationController
redirect_to start_form_path(@template.slug)
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
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|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
def load_template
@template =
if @resubmit_submitter
@resubmit_submitter.template
else
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
@template = Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
def multiple_submitters_error_message
@@ -224,38 +146,31 @@ class StartFormController < ApplicationController
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?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
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
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request, is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
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:)
render :email_verification
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
redirect_to start_form_path(submitter.submission.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
rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end
end
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController
layout 'form'
around_action :with_browser_locale
skip_before_action :authenticate_user!
@@ -13,12 +15,20 @@ class StartFormEmail2faSendController < ApplicationController
@submitter = @template.submissions.new(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:)
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)),
**redir_params
rescue Submitters::BouncedEmail => e
@error_message = e.message
render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
@@ -36,14 +46,24 @@ class StartFormEmail2faSendController < ApplicationController
return redirect_to start_form_path(@template.slug) if is_archived
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
@template.preferences['shared_link_2fa'] == true
(@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)
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
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
@@ -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
def submitter_slug
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id]
params[:submit_form_slug] || params[:submitter_slug]
end
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 = submitter_version.submitter
maybe_render_locked_page
end
private
@@ -19,14 +19,15 @@ class SubmitFormDeclineController < ApplicationController
request)
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] })
end
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!
end
@@ -32,7 +32,8 @@ class SubmitFormDelegateController < ApplicationController
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
{ 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
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@@ -8,9 +8,6 @@ class SubmitFormEmail2fasController < ApplicationController
before_action :load_submitter
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def create
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 })
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)
else
@@ -37,6 +35,12 @@ class SubmitFormEmail2fasController < ApplicationController
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)
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)
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
@submitter.submission.update!(submitters_order: :preserved)
@@ -13,9 +13,10 @@ class TemplatesDebugController < ApplicationController
data = attachment.download
unless attachment.image?
pdf = HexaPDF::Document.new(io: StringIO.new(data))
fields = Templates::FindAcroFields.call(pdf, attachment, data)
fields =
Pdfium::Document.open_io(StringIO.new(data)) do |doc|
Templates::FindPdfiumAcroFields.call(attachment, doc, data)
end
end
# fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
@@ -10,8 +10,14 @@ class TemplatesDetectFieldsController < ApplicationController
sse = SSE.new(response.stream)
documents = @template.schema_documents.preload(:blob)
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
documents =
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
@@ -63,7 +63,7 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
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]))
file = ActionDispatch::Http::UploadedFile.new(
@@ -6,6 +6,7 @@ class TestingAccountsController < ApplicationController
def create
authorize!(:manage, current_account)
authorize!(:manage, current_user)
authorize!(:manage, EncryptedConfig)
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?
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."
end
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
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?
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."
end
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
+9 -1
View File
@@ -3,11 +3,19 @@
class WebhookSecretController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false
HEADER_NAME_REGEXP = /\A[\w-]+\z/
def show; end
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_secret_params[:key] => webhook_secret_params[:value]
key => webhook_secret_params[:value]
}.compact_blank)
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 decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => {
if (!node.isText) return
+1 -1
View File
@@ -150,7 +150,7 @@ export default actionable(targetable(class extends HTMLElement {
const buildDecorations = (doc) => {
const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => {
if (!node.isText) return
+1 -1
View File
@@ -230,7 +230,7 @@ export default {
download () {
this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
fetch(this.baseUrl + `/s/${this.submitterSlug}/documents`, {
method: 'GET',
...this.fetchOptions
}).then(async (response) => {
+4 -1
View File
@@ -560,6 +560,7 @@
:fields="formulaFields"
:values="values"
:readonly-values="readonlyFieldValues"
:fetch-options="fetchOptions"
:provider="paymentProvider"
@attached="attachments.push($event)"
@focus="scrollIntoField(currentField)"
@@ -573,6 +574,7 @@
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:fetch-options="fetchOptions"
:values="values"
@submit="!isSubmitting && submitStep()"
/>
@@ -585,6 +587,7 @@
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:fetch-options="fetchOptions"
:values="values"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
@@ -1776,7 +1779,7 @@ export default {
window.location.href = sanitizeUrl(this.completedRedirectUrl)
} else {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
const root = this.$root.$el.parentNode?.getRootNode() || document
const completedEl = root.getElementById('form_completed')
if (completedEl) {
+10 -3
View File
@@ -312,6 +312,11 @@ export default {
values: {
type: Object,
required: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
}
},
emits: ['submit'],
@@ -494,8 +499,9 @@ export default {
const resp = await fetch(this.baseUrl + '/api/kba', {
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()
@@ -550,7 +556,8 @@ export default {
reference: this.reference,
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
})
const data = await resp.json()
@@ -121,6 +121,11 @@ export default {
type: String,
required: false,
default: ''
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
@@ -234,7 +239,8 @@ export default {
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
this.isProcessing = false
@@ -274,7 +280,8 @@ export default {
check_status: checkStatus,
referer: this.normalizedPaymentUrl()
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
@@ -261,7 +261,7 @@ export default {
}
},
resendCode () {
if (this.codeSentAt && Date.now() - this.codeSentAt < 15000) {
if (this.codeSentAt && Date.now() - this.codeSentAt < 30000) {
this.startResendCodeCountdown()
} else {
this.isResendLoading = true
@@ -274,7 +274,7 @@ export default {
}
},
startResendCodeCountdown () {
this.resendCodeCountdown = 15 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.resendCodeCountdown = 30 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.interval = setInterval(() => {
this.resendCodeCountdown--
@@ -91,6 +91,11 @@ export default {
submitterSlug: {
type: String,
required: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
@@ -163,7 +168,8 @@ export default {
submitter_slug: this.submitterSlug,
redirect_url: document.location.href
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => {
this.eidEasyData = await resp.json()
@@ -205,7 +211,8 @@ export default {
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
})
if (resp.status === 404) {
+19 -11
View File
@@ -110,7 +110,7 @@
data-turbo="false"
class="inline"
method="post"
:action="`/d/${template.slug}`"
:action="`/start_form_self/${template.id}`"
@submit="maybeShowErrorTemplateAlert"
>
<input
@@ -125,12 +125,6 @@
:value="authenticityToken"
autocomplete="off"
>
<input
type="hidden"
name="selfsign"
value="true"
autocomplete="off"
>
<button
class="btn btn-primary btn-ghost text-base hidden md:flex"
type="submit"
@@ -2377,7 +2371,6 @@ export default {
const areaCopy = JSON.parse(JSON.stringify(area))
delete fieldCopy.areas
delete fieldCopy.submitter_uuid
areaCopy.relativeX = area.x - minX
areaCopy.relativeY = area.y - minY
@@ -2387,6 +2380,7 @@ export default {
const clipboardData = {
items,
submitters: this.template.submitters.map((submitter) => ({ uuid: submitter.uuid, name: submitter.name })),
templateId: this.template.id,
timestamp: Date.now(),
isGroup: true
@@ -2497,6 +2491,20 @@ export default {
const fieldUuidIndex = {}
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) => {
const field = JSON.parse(JSON.stringify(item.field))
@@ -2521,7 +2529,7 @@ export default {
const newField = fieldUuidIndex[field.uuid] || {
...field,
uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid,
submitter_uuid: submitterUuidsMap[field.submitter_uuid] || this.selectedSubmitter.uuid,
areas: []
}
@@ -3531,6 +3539,8 @@ export default {
this.onChange(this.template)
}
this.pushUndo()
if (!this.autosave && !force) {
return Promise.resolve({})
}
@@ -3541,8 +3551,6 @@ export default {
}
})
this.pushUndo()
return this.baseFetch(`/templates/${this.template.id}`, {
method: 'PUT',
body: JSON.stringify({
+8 -5
View File
@@ -555,8 +555,10 @@ export default {
return this.template.schema.some((item) => item.dynamic)
},
numberOfPages () {
return this.template.documents.reduce((acc, doc) => {
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
return this.template.schema.reduce((acc, item) => {
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)
},
isShowFieldSearch () {
@@ -774,9 +776,10 @@ export default {
headers: {
'Content-Type': 'application/json'
},
...(this.withDetectExistingFields
? { body: JSON.stringify({ fields: this.buildExistingFields() }) }
: {})
body: JSON.stringify({
attachment_uuid: this.template.schema.map((item) => item.attachment_uuid),
...(this.withDetectExistingFields ? { fields: this.buildExistingFields() } : {})
})
}).then(async (response) => {
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
@@ -366,6 +366,9 @@ export default {
}
}
},
beforeUnmount () {
document.getElementById('list_form_buttons')?.classList?.add('hidden')
},
methods: {
t (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 }
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.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
SubmitterMailer.completed_email(submitter, user).deliver_later!
+2
View File
@@ -31,6 +31,8 @@ class EmailMessage < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid }
normalizes :body, with: ->(value) { value.to_s }, apply_to_nil: true
before_validation :set_sha1, on: :create
def normalized_body
+2 -1
View File
@@ -112,7 +112,8 @@ class Submission < ApplicationRecord
api: 'api',
embed: 'embed',
mcp: 'mcp',
link: 'link'
link: 'link',
self: 'self'
}, scope: false, prefix: true
enum :submitters_order, {
+1 -1
View File
@@ -90,7 +90,7 @@ class Submitter < ApplicationRecord
def friendly_name
if name.present? && email.present? && email.exclude?(',')
%("#{name.delete('"')}" <#{email}>)
%("#{name.delete('"').squish}" <#{email}>)
else
email
end
@@ -3,7 +3,7 @@
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %>
<span class="flex items-center">
<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') %>
</div>
</span>
@@ -14,7 +14,7 @@
</a>
</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 %>
<%= t('make_default') %>
<% end %>
+1 -1
View File
@@ -120,7 +120,7 @@
<span>
<%= t('timeserver_url') %>
</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') %>
</span>
</span>
+4 -6
View File
@@ -6,12 +6,10 @@
<%= t('mcp_server') %>
</h1>
<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 %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_token') %></span>
<% end %>
</div>
<%= 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') %>
<span><%= t('new_token') %></span>
<% end %>
</div>
</div>
<% if @mcp_token.present? %>
@@ -21,13 +21,15 @@
<% end %>
</div>
<%= render 'bcc_form', config: @bcc_config %>
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-2xl md:text-3xl font-bold">
<%= t('sign_request_email_reminders') %>
</h2>
</div>
<%= render 'reminder_banner' %>
<%= render 'reminder_form', config: @reminder_config %>
<% unless can?(:manage, :personalization_restricted) %>
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-2xl md:text-3xl font-bold">
<%= t('sign_request_email_reminders') %>
</h2>
</div>
<%= render 'reminder_banner' %>
<%= render 'reminder_form', config: @reminder_config %>
<% end %>
</div>
<div class="w-0 md:w-52"></div>
</div>
@@ -6,11 +6,16 @@
</div>
</div>
<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.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">
<%= 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' %>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
@@ -6,7 +6,7 @@
</div>
</div>
<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.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
<div class="form-control">
@@ -6,7 +6,7 @@
</div>
</div>
<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.fields_for :value, Struct.new(:title, :body).new(*(f.object.value || {}).values_at('title', 'body')) do |ff| %>
<div class="form-control">
@@ -7,7 +7,7 @@
</div>
</div>
<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 %>
<div class="form-control">
<autoresize-textarea>
@@ -6,11 +6,16 @@
</div>
</div>
<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.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">
<%= 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' %>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
@@ -6,13 +6,13 @@
</div>
</div>
<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.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="flex items-center">
<%= 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') %>
</span>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'banner' %>
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<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' %>
</toggle-submit>
<% 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">
<%= 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>
@@ -23,12 +23,12 @@
<div>
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
</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">
<%= f.hidden_field 'resubmit', value: params[:resubmit] %>
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
<%= f.hidden_field 'submitter[name]', value: params[:name] || params.dig(:submitter, :name) %>
<%= f.hidden_field 'submitter[email]', value: params[:email] || params.dig(:submitter, :email) %>
<%= 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' %>
<div class="flex justify-between items-center mt-1">
<span>
@@ -49,6 +49,10 @@
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
</toggle-submit>
<% 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>
</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 %>
<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">
<%= render 'banner' %>
<%= render 'start_form/banner' %>
<p class="text-xl font-semibold text-center">
<%= t('share_link_is_currently_disabled') %>
</p>
+3 -2
View File
@@ -6,7 +6,7 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="text-center w-full space-y-6">
<%= render 'banner' %>
<%= render 'start_form/banner' %>
<% unless @template.archived_at? %>
<p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p>
<% end %>
@@ -28,7 +28,8 @@
</div>
</div>
<% 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 %>
<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') %>
+1 -1
View File
@@ -36,7 +36,7 @@
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</span>
<% 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') %>
</span>
<% end %>
@@ -7,7 +7,7 @@
<%= 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]) %>
<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') %>
</span>
<% end %>
@@ -17,7 +17,7 @@
</div>
</div>
<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: %>
</div>
</div>
@@ -28,7 +28,7 @@
</div>
</div>
<% 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>
<a href="<%= submissions_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
</div>
@@ -3,15 +3,16 @@
<% 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.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' } %>
<% 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? %>
<% chips_html = capture do %>
<% if 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">
<% status_chip_html = capture do %>
<% 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 <%= '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 %>
<%= 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>
<% 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 %>
@@ -19,6 +20,8 @@
<% end %>
</div>
<% end %>
<% end %>
<% chips_html = capture do %>
<% 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') %>">
<%= 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>
<% 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">
<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') %>
@@ -122,15 +126,16 @@
</ul>
</div>
<% end %>
<% if chips_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">
<% if with_status_button %>
<% 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 <%= 'max-md:hidden' if with_status_dropdown %>">
<% 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' %>">
<%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %>
<span><%= t(current_status) %></span>
<%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %>
</a>
<% end %>
<%= status_chip_html %>
<%= chips_html %>
</scroll-fade>
<% end %>
+1 -1
View File
@@ -44,7 +44,7 @@
<% end %>
</div>
<% 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>
<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' %>
+2 -2
View File
@@ -56,7 +56,7 @@
<div class="mockup-code overflow-hidden pb-0 mt-4">
<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' %>
<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>
">
@@ -78,7 +78,7 @@
</clipboard-copy>
</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>
</code></pre>
+3 -3
View File
@@ -35,14 +35,14 @@
<div class="flex items-center space-x-4">
<span class="flex flex-col md:flex-row md:items-center gap-3">
<% 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">
<%= t('expired') %>
</span>
</div>
<% else %>
<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) %>
</span>
</a>
@@ -125,7 +125,7 @@
</span>
</a>
<% 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">
<%= t('expired') %>
</span>
+6 -5
View File
@@ -64,8 +64,12 @@
<% end %>
</div>
<div class="flex flex-wrap gap-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1">
<% if !template.archived_at? && can?(:destroy, template) %>
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving')[..-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 can?(:destroy, template) %>
<% 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 %>
<% if can?(:create, current_account.templates.new(author: current_user)) %>
<div class="flex-1">
@@ -99,9 +103,6 @@
<% end %>
<% end %>
<% 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">
<%= 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">
+1 -1
View File
@@ -99,7 +99,7 @@
</open-modal-mobile>
<% end %>
<% 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') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %>
@@ -19,7 +19,7 @@
</div>
</div>
<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">
<%= 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') %>
+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)) %>
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<dashboard-dropzone>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= 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">
<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' %>
<span class="flex items-center mt-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| %>
<div class="form-control">
<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 %>
<label for="submitter_completed_email_reset_link" class="label underline">
<%= 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| %>
<div class="form-control">
<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 %>
<label for="submitter_documents_copy_email_reset_link" class="label underline">
<%= 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| %>
<div class="form-control">
<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 %>
<label for="submitter_invitation_email_reset_link" class="label underline">
<%= 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| %>
<div class="form-control">
<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 %>
<label for="submitter_view_invitation_email_reset_link" class="label underline">
<%= t('reset_default') %>
+1 -1
View File
@@ -10,7 +10,7 @@
<% 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| %>
<% 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>
<%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %>
</label>
+1 -1
View File
@@ -24,7 +24,7 @@
<% end %>
<% end %>
<% 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 %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>
+1 -1
View File
@@ -83,7 +83,7 @@
</div>
<% if @webhook_events.present? || params[:status].present? %>
<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">
<%= 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]'}" %>
+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', '')}")
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)
end
+1 -1
View File
@@ -2,7 +2,7 @@
autoload :CSV, 'csv'
autoload :CSVSafe, 'csv-safe'
autoload :RubyXL, 'rubyXL'
autoload :Xlsxtream, 'xlsxtream'
autoload :Zip, 'zip'
autoload :Numo, 'numo/narray'
autoload :OnnxRuntime, 'onnxruntime'
+61
View File
@@ -46,6 +46,20 @@ module HexaPDF
def color_space(name)
GlobalConfiguration.constantize('color_space.map', name).new
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
# fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field
@@ -112,6 +126,8 @@ module HexaPDF
end
module CycleSafeInheritedValue
using HexaPDF::Type::AcroForm::Field::HashRefinement
def inherited_value(field, name)
seen = Set.new.compare_by_identity
seen << field.value
@@ -167,8 +183,53 @@ module HexaPDF
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
HexaPDF::Type::AcroForm::Field.singleton_class.prepend(HexaPDF::CycleSafeInheritedValue)
HexaPDF::Type::AcroForm::Field.prepend(HexaPDF::CycleSafeFullFieldName)
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.
enter_the_verification_code_from_your_email: Enter the verification code from your email.
too_many_attempts: Too many attempts.
verification_email_bounced: "Unable to send the verification code: email to this address bounced."
verification_code: Verification Code
resend_code: Resend Code
verify_new_sign_in: Verify new sign in
@@ -992,6 +993,13 @@ en: &en
open_file_from: 'Open file from'
cancel: Cancel
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:
confirmations:
confirmed: Your email address has been successfully confirmed.
@@ -1007,6 +1015,7 @@ en: &en
invite: Invite
link: Link
mcp: MCP
self: Sign Yourself
submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{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_redirect_button: Botón de redirección de formulario completado
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
subject: Asunto
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.
enter_the_verification_code_from_your_email: Ingresa el código de verificación de tu correo electrónico.
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
resend_code: Reenviar Código
verify_new_sign_in: Verificar nuevo inicio de sesión
@@ -2118,6 +2135,7 @@ es: &es
invite: Invitación
link: Enlace
mcp: MCP
self: Firma tú mismo
submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{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_redirect_button: Pulsante di reindirizzamento dopo il completamento del modulo
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
subject: Oggetto
body: Corpo
@@ -3026,6 +3051,7 @@ it: &it
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.
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
resend_code: Reinvia Codice
verify_new_sign_in: Verifica nuovo accesso
@@ -3243,6 +3269,7 @@ it: &it
invite: Invito
link: Link
mcp: MCP
self: Firma te stesso
submission_event_names:
send_email_to_html: '<b>Email inviata</b> a %{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_redirect_button: Bouton de redirection après finalisation
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
subject: Objet
body: Corps
@@ -4137,6 +4171,7 @@ fr: &fr
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.
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
resend_code: Renvoyer le code
verify_new_sign_in: Vérifier une nouvelle connexion
@@ -4351,6 +4386,7 @@ fr: &fr
invite: Invitation
link: Lien
mcp: MCP
self: Signer vous-même
submission_event_names:
send_email_to_html: "<b>E‑mail envoyé</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_redirect_button: Botão de redirecionamento do formulário concluído
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
subject: Assunto
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.
enter_the_verification_code_from_your_email: Insira o código de verificação do seu e-mail.
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
resend_code: Reenviar Código
verify_new_sign_in: Verificar novo login
@@ -5475,6 +5519,7 @@ pt: &pt
invite: Convite
link: Link
mcp: MCP
self: Assine você mesmo
submission_event_names:
send_email_to_html: '<b>Email enviado</b> para %{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_redirect_button: Weiterleitungsschaltfläche nach Formularabschluss
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
subject: Betreff
body: Inhalt
@@ -6382,6 +6434,7 @@ de: &de
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.
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
resend_code: Code erneut senden
verify_new_sign_in: Neue Anmeldung verifizieren
@@ -6599,6 +6652,7 @@ de: &de
invite: Einladung
link: Link
mcp: MCP
self: Selbst unterschreiben
submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{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.
enter_the_verification_code_from_your_email: Wprowadź kod weryfikacyjny z Twojego e-maila.
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
resend_code: Wyślij Kod Ponownie
powered_by: 'Napędzany przez'
@@ -6960,6 +7016,8 @@ uk:
the_code_has_been_sent_to_your_email: Код було надіслано на вашу електронну пошту.
enter_the_verification_code_from_your_email: Введіть код перевірки з вашої електронної пошти.
too_many_attempts: Забагато спроб.
verification_email_bounced: "Не вдалося надіслати код підтвердження: лист на цю адресу було відхилено."
back: Назад
verification_code: Код перевірки
resend_code: Надіслати код знову
powered_by: 'Працює на базі'
@@ -7100,6 +7158,8 @@ cs:
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.
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
resend_code: Znovu odeslat kód
powered_by: 'Poháněno'
@@ -7226,6 +7286,8 @@ he:
the_code_has_been_sent_to_your_email: הקוד נשלח לדוא"ל שלך.
enter_the_verification_code_from_your_email: הזן את קוד האימות מדוא"ל שלך.
too_many_attempts: יותר מדי ניסיונות.
verification_email_bounced: "לא ניתן לשלוח את קוד האימות: האימייל לכתובת זו נדחה."
back: חזרה
verification_code: קוד אימות
resend_code: שלח קוד מחדש
powered_by: 'מופעל על ידי'
@@ -7595,6 +7657,13 @@ nl: &nl
completed_form_message: Bericht voltooid formulier
completed_form_redirect_button: Knop voor doorverwijzing na voltooid formulier
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
subject: Onderwerp
body: Inhoud
@@ -7965,7 +8034,7 @@ nl: &nl
awaiting: Wachten
document_id: Document-ID
envelope_id: Envelop-ID
event_log: Gebeurtenissenlogboek
event_log: Gebeurtenislogboek
verify: Verifiëren
testing_log_not_for_production_use: Testlogboek - Niet voor productiegebruik
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.
enter_the_verification_code_from_your_email: Voer de verificatiecode uit uw e-mail in.
too_many_attempts: Te veel pogingen.
verification_email_bounced: "Verificatiecode kan niet worden verzonden: e-mail naar dit adres is geretourneerd."
verification_code: Verificatiecode
resend_code: Code opnieuw verzenden
verify_new_sign_in: Nieuwe aanmelding verifiëren
@@ -8239,6 +8309,7 @@ nl: &nl
invite: Uitnodiging
link: Link
mcp: MCP
self: Zelf ondertekenen
submission_event_names:
send_email_to_html: "<b>E-mail verzonden</b> naar %{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: تم إرسال الرمز إلى بريدك الإلكتروني.
enter_the_verification_code_from_your_email: أدخل رمز التحقق من بريدك الإلكتروني.
too_many_attempts: عدد المحاولات كبير جدًا.
verification_email_bounced: "تعذر إرسال رمز التحقق: تم رفض البريد الإلكتروني المرسل إلى هذا العنوان."
back: رجوع
verification_code: رمز التحقق
resend_code: إعادة إرسال الرمز
powered_by: 'مشغل بواسطة'
@@ -8559,6 +8632,8 @@ ko:
the_code_has_been_sent_to_your_email: 코드가 이메일로 전송되었습니다.
enter_the_verification_code_from_your_email: 이메일로 받은 인증 코드를 입력하세요.
too_many_attempts: 시도 횟수가 너무 많습니다.
verification_email_bounced: "인증 코드를 보낼 수 없습니다: 이 주소로 보낸 이메일이 반송되었습니다."
back: 뒤로
verification_code: 인증 코드
resend_code: 코드 재전송
powered_by: '제공:'
@@ -8671,6 +8746,8 @@ ja:
the_code_has_been_sent_to_your_email: コードがあなたのメールに送信されました
enter_the_verification_code_from_your_email: メールに記載された認証コードを入力してください
too_many_attempts: 試行回数が多すぎます
verification_email_bounced: "認証コードを送信できません:このアドレス宛のメールが配信できませんでした。"
back: 戻る
verification_code: 認証コード
resend_code: コードを再送信
powered_by: '提供元:'
+6 -2
View File
@@ -142,7 +142,8 @@ Rails.application.routes.draw do
get :completed
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]
resources :start_form_email_2fa_send, only: :create
@@ -174,10 +175,13 @@ Rails.application.routes.draw do
resources :submitters, only: %i[] do
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'
end
resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submit_form_completed_download'
end
resources :settings, only: %i[index]
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.
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
enable_extension "btree_gin"
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
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}
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
UnsupportedFormat = Class.new(StandardError)
module_function
def load_vips(data, content_type: nil, autorot: false)
content_type ||= Marcel::MimeType.for(data)
if ICO_REGEXP.match?(content_type)
LoadIco.call(data)
elsif BMP_REGEXP.match?(content_type)
LoadBmp.call(data)
else
image = Vips::Image.new_from_buffer(data, '')
image =
case content_type
when PNG_REGEXP
Vips::Image.pngload_buffer(data)
when JPEG_REGEXP
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
end
autorot ? image.autorot : image
end
def blank?(image)
+34 -78
View File
@@ -3,6 +3,9 @@
module LoadBmp
BPPS = [1, 4, 8, 24, 32].freeze
MAX_COORD = ENV.fetch('VIPS_MAX_COORD', '17000').to_i
MAX_PIXELS = 145_000_000
module_function
# rubocop:disable Metrics
@@ -18,40 +21,28 @@ module LoadBmp
header_data[:height]
)
if header_data[:bpp] <= 8
final_pixel_data = decode_indexed_pixel_data(
raw_pixel_data_from_file,
header_data[:bpp],
header_data[:width],
header_data[:height],
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
padded_rows = Vips::Image.new_from_memory_copy(
raw_pixel_data_from_file,
header_data[:bmp_stride],
header_data[:height],
1,
:uchar
)
image_rgb =
if header_data[:bpp] <= 8
image
elsif bands == 3
image.recomb(band3_recomb)
elsif bands == 4
image.recomb(band4_recomb)
decode_indexed_pixel_data(padded_rows, header_data[:bpp], header_data[:width], header_data[:color_table])
else
bands = header_data[:bpp] / 8
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
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
@@ -98,6 +89,9 @@ module LoadBmp
raise ArgumentError, 'BMP width must be positive.' if width <= 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
raise ArgumentError,
@@ -164,62 +158,24 @@ module LoadBmp
raw_pixel_data_from_file
end
def prepare_unpadded_pixel_data_string(raw_pixel_data_from_file, bpp, width, height, bmp_stride)
bytes_per_pixel = bpp / 8
actual_row_width_bytes = width * bytes_per_pixel
def decode_indexed_pixel_data(padded_rows, bpp, width, color_table)
pixels_per_byte = 8 / bpp
unpadded_rows = Array.new(height)
current_offset_in_blob = 0
image = padded_rows.maplut(build_palette_lut(bpp, color_table))
image = image.bandunfold.bandfold(factor: 3) if pixels_per_byte > 1
height.times do |i|
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
image.extract_area(0, 0, width, padded_rows.height)
end
def decode_indexed_pixel_data(raw_data, bpp, width, height, bmp_stride, color_table)
palette = color_table.map { |r, g, b| [r, g, b].pack('CCC') }
def build_palette_lut(bpp, color_table)
pixels_per_byte = 8 / bpp
mask = (1 << bpp) - 1
output = String.new(capacity: width * height * 3)
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
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] }
end
output
Vips::Image.new_from_memory_copy(entries.flatten.pack('C*'), 256, 1, pixels_per_byte * 3, :uchar)
end
def band3_recomb
+1 -1
View File
@@ -94,7 +94,7 @@ module LoadIco
palette = []
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
palette_color_bytes = dib_io.read(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|
type(message_params, :subject, String)
type(message_params, :body, String)
required(message_params, :body)
end
end
@@ -53,8 +51,6 @@ module Params
in_path(params, :message, skip_blank: true) do |message_params|
type(message_params, :subject, String)
type(message_params, :body, String)
required(message_params, :body)
end
value_in(params, :order, %w[preserved random], allow_nil: true)
@@ -119,8 +115,6 @@ module Params
in_path(params, :message) do |message_params|
type(message_params, :subject, String)
type(message_params, :body, String)
required(message_params, :body)
end
value_in(params, :order, %w[preserved random], allow_nil: true)
-18
View File
@@ -6,24 +6,6 @@ module PdfUtils
module_function
def encrypted?(data, password: nil)
HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
false
rescue HexaPDF::EncryptionError
true
end
def decrypt(data, password)
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)
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_CloseDocument, [:FPDF_DOCUMENT], :void
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_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong
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_GetFormType, [:FPDF_DOCUMENT], :int
@@ -282,6 +284,7 @@ class Pdfium
attach_function :FPDFPage_GetAnnotCount, [:FPDF_PAGE], :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_CloseAnnot, [:FPDF_ANNOTATION], :void
attach_function :FPDFAnnot_GetSubtype, [:FPDF_ANNOTATION], :int
@@ -368,6 +371,11 @@ class Pdfium
:bottom, :float
end
class FS_SIZEF < FFI::Struct
layout :width, :float,
:height, :float
end
class FS_MATRIX < FFI::Struct
layout :a, :float,
:b, :float,
@@ -531,6 +539,8 @@ class Pdfium
@pages = {}
@annot_counts = {}
@page_sizes = {}
@page_rotations = {}
@closed = false
@source_buffer = source_buffer
@form_handle = FFI::Pointer::NULL
@@ -555,6 +565,27 @@ class Pdfium
@page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr)
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?
Pdfium.FPDF_GetSecurityHandlerRevision(@document_ptr) >= 0
end
@@ -745,6 +776,10 @@ class Pdfium
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
end
def only_dss_changes?(signed_document)
Pdfium.FPDF_HasOnlyDSSChanges(@document_ptr, signed_document.document_ptr) == 1
end
def annot_count(page_index)
@annot_counts[page_index] ||= Pdfium.FPDFPage_GetAnnotCountRaw(@document_ptr, page_index)
end
@@ -983,6 +1018,9 @@ class Pdfium
def rotation=(value)
Pdfium.FPDFPage_SetRotation(@page_ptr, value)
@document.reset_page_size(@page_index)
@document.reset_page_rotation(@page_index)
@rotation = value
end
@@ -2278,14 +2316,14 @@ class Pdfium
[PageObject, *to_a].hash
end
private
def object_ptr
page.ensure_not_closed!
Pdfium.FPDFPage_GetObject(page.page_ptr, index)
end
private
def read_bounds
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?
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
'--bind', '127.0.0.1', '--port', '16379',
'--loglevel', 'warning')
end
end
+2
View File
@@ -236,6 +236,8 @@ module SearchEntries
end
def add_hyphens(entry, text)
text = text.tr('\\', ' ')
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
hyphens.uniq.each_with_index do |item, index|
+14 -3
View File
@@ -3,6 +3,11 @@
module Submissions
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
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 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?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<'))
@@ -220,8 +225,7 @@ module Submissions
domain = email.split('@').last.to_s.downcase
fixed_domain = fixed_email.to_s.split('@').last
return email.downcase if domain == fixed_domain
return email.downcase if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
return email.downcase if domain == fixed_domain || skip_email_fix?(domain, fixed_domain)
threshold = fixed_domain.start_with?('hotmail.') ? 2 : 3
@@ -236,6 +240,13 @@ module Submissions
fixed_email
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)
(submission.template_schema || submission.template.schema).filter_map do |item|
if item['conditions'].present?
+20 -10
View File
@@ -19,6 +19,7 @@ module Submissions
}.freeze
TESTING_FOOTER = GenerateResultAttachments::TESTING_FOOTER
UNSUPPORTED_IMAGE_TYPES = GenerateResultAttachments::UNSUPPORTED_IMAGE_TYPES
RTL_REGEXP = TextUtils::RTL_REGEXP
MAX_IMAGE_HEIGHT = 100
@@ -61,7 +62,7 @@ module Submissions
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
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',
record: submission
@@ -219,7 +220,7 @@ module Submissions
[
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(
[
@@ -272,7 +273,9 @@ module Submissions
[
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.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\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['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" },
"\n"
].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0]
@@ -352,8 +355,9 @@ module Submissions
field_type = field['type']
if field_type == 'image' &&
submitter.attachments.find { |a| a.uuid == value }.then { |a| !a.image? || a.content_type == 'image/heic' }
if (field_type == 'image' || field_type == 'stamp') &&
submitter.attachments.find { |a| a.uuid == value }
.then { |a| !a.image? || a.content_type.in?(UNSUPPORTED_IMAGE_TYPES) }
field_type = 'file'
end
@@ -425,7 +429,7 @@ module Submissions
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
end
{ link:, text: "#{attachment.filename}\n", style: :link }
{ link:, text: "#{TextUtils.maybe_rtl_reverse(attachment.filename.to_s)}\n", style: :link }
end,
padding: [0, 0, 10, 0]
)
@@ -476,6 +480,8 @@ module Submissions
submitter.name || submitter.email || submitter.phone
end
submitter_name = TextUtils.maybe_rtl_reverse(submitter_name.to_s.squish)
text =
if event.event_type == 'complete_verification'
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' &&
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
(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,
name].join(' ')
invited_submitter_name = TextUtils.maybe_rtl_reverse(
[invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' ').squish
)
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
submitter_name:)
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:),
"<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")
elsif event.event_type == 'delegate_form'
from = event.data['old_email'].presence ||
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'])
elsif event.event_type.include?('send_')
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
@@ -37,7 +37,7 @@ module Submissions
ActiveStorage::Attachment.create!(
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',
record: submission
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more