mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
27
Commits
11856a17ac
..
3.2.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47c090e1f1 | ||
|
|
678997fb36 | ||
|
|
24f195c7e5 | ||
|
|
5741bb4db2 | ||
|
|
1d169668f0 | ||
|
|
ef3d5bec7e | ||
|
|
d578ca3cc9 | ||
|
|
5432978c0d | ||
|
|
de1af49348 | ||
|
|
2f0bcb4a80 | ||
|
|
11f27c8cf0 | ||
|
|
6e2430e5a2 | ||
|
|
85fbae1813 | ||
|
|
9941122d90 | ||
|
|
71be6db358 | ||
|
|
994c1b3155 | ||
|
|
92b297a620 | ||
|
|
bd03498b0f | ||
|
|
660b24710e | ||
|
|
1416cf72e8 | ||
|
|
ae7b0ce608 | ||
|
|
a3828ec390 | ||
|
|
b8c1691bd7 | ||
|
|
4b00c896e4 | ||
|
|
781da32a07 | ||
|
|
307036eae6 | ||
|
|
9944a9b1a5 |
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,6 +46,8 @@ module Mcp
|
||||
|
||||
authorize!(:create, @template)
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if mcp_params['url'].present?
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -26,7 +26,8 @@ class SubmitFormDeclineController < ApplicationController
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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--
|
||||
|
||||
@@ -2371,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
|
||||
@@ -2381,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
|
||||
@@ -2491,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))
|
||||
@@ -2515,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: []
|
||||
}
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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? %>
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
</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">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
|
||||
<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>
|
||||
|
||||
@@ -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,13 +6,13 @@
|
||||
</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">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
|
||||
<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>
|
||||
|
||||
@@ -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 tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %> <%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
|
||||
<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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="<%= Docuseal::CDN_URL %>/js/form.js"></script>
|
||||
<clipboard-copy data-text="<script src="<%= Docuseal.multitenant? ? "#{Docuseal::CDN_URL}/js/form.js" : embed_script_url(filename: 'form.js') %>"></script>
|
||||
|
||||
<docuseal-form data-src="<%= start_form_url(slug: template.slug) %>"></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"><script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">></script></span>
|
||||
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75"><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">></script></span>
|
||||
|
||||
<span style="color: #f4bf75"><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">></span><span style="color: #f4bf75"></docuseal-form></span>
|
||||
</code></pre>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %>">
|
||||
<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>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
|
||||
<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>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
|
||||
<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>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('view_only_email_sent_to_recipients_that_need_to_only_review_the_documents') %>">
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -8034,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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -109,6 +109,7 @@ class Pdfium
|
||||
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
|
||||
|
||||
@@ -581,6 +582,10 @@ class Pdfium
|
||||
@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
|
||||
@@ -771,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
|
||||
@@ -1010,6 +1019,7 @@ class Pdfium
|
||||
Pdfium.FPDFPage_SetRotation(@page_ptr, value)
|
||||
|
||||
@document.reset_page_size(@page_index)
|
||||
@document.reset_page_rotation(@page_index)
|
||||
|
||||
@rotation = value
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -62,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,22 +19,19 @@ module Submissions
|
||||
end
|
||||
|
||||
def rows_to_xlsx(rows)
|
||||
workbook = RubyXL::Workbook.new
|
||||
worksheet = workbook[0]
|
||||
worksheet.sheet_name = Time.current.to_date.to_s
|
||||
|
||||
headers = build_headers(rows)
|
||||
headers.each_with_index do |column_name, column_index|
|
||||
worksheet.add_cell(0, column_index, column_name)
|
||||
end
|
||||
|
||||
rows.each.with_index(1) do |row, row_index|
|
||||
extract_columns(row, headers).each_with_index do |value, column_index|
|
||||
worksheet.add_cell(row_index, column_index, value)
|
||||
io = StringIO.new
|
||||
|
||||
Xlsxtream::Workbook.open(io) do |workbook|
|
||||
workbook.write_worksheet(Time.current.to_date.to_s) do |sheet|
|
||||
sheet << headers.to_a
|
||||
|
||||
rows.each { |row| sheet << extract_columns(row, headers) }
|
||||
end
|
||||
end
|
||||
|
||||
workbook.stream.string
|
||||
io.string
|
||||
end
|
||||
|
||||
def rows_to_csv(rows)
|
||||
@@ -150,8 +147,6 @@ module Submissions
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||
end
|
||||
elsif submitter_value == true || submitter_value == false
|
||||
submitter_value.to_s
|
||||
else
|
||||
submitter_value
|
||||
end
|
||||
|
||||
@@ -71,7 +71,7 @@ module Submissions
|
||||
submission:,
|
||||
values_hash:,
|
||||
name: 'preview_merged_document',
|
||||
filename: "#{submission.name || template&.name}.pdf"
|
||||
filename: "#{submission.name || template&.name}.pdf".tr('/', '-')
|
||||
)
|
||||
|
||||
ApplicationRecord.no_touching { attachment.save! }
|
||||
@@ -115,7 +115,7 @@ module Submissions
|
||||
submitter:,
|
||||
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
|
||||
values_hash:,
|
||||
filename: "#{submission.name || template&.name}.pdf"
|
||||
filename: "#{submission.name || template&.name}.pdf".tr('/', '-')
|
||||
)
|
||||
|
||||
ApplicationRecord.no_touching do
|
||||
|
||||
@@ -814,7 +814,7 @@ module Submissions
|
||||
end
|
||||
|
||||
ActiveStorage::Attachment.new(
|
||||
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf"),
|
||||
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf".tr('/', '-')),
|
||||
metadata: { original_uuid: uuid,
|
||||
analyzed: true,
|
||||
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
|
||||
|
||||
+18
-15
@@ -16,16 +16,6 @@ module Submitters
|
||||
MaliciousFileExtension = Class.new(StandardError)
|
||||
ParamsError = Class.new(StandardError)
|
||||
|
||||
DANGEROUS_EXTENSIONS = Set.new(%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 bat cmd vb vba
|
||||
sh bash zsh fish run out bin elf gadget workflow lnk scf
|
||||
url desktop application action workflow apk ipa xap appx
|
||||
appxbundle msix msixbundle diagcab diagpkg cpl msc ocx
|
||||
drv scr ins isp mst paf prf shb shs slk ws wsc inf1 inf2
|
||||
].freeze)
|
||||
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
module_function
|
||||
@@ -127,11 +117,9 @@ module Submitters
|
||||
def create_attachment!(submitter, file, metadata: {})
|
||||
raise ParamsError, 'file param is missing' if file.blank?
|
||||
|
||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||
extension = FilenameUtils.dangerous_extension(file.original_filename)
|
||||
|
||||
if DANGEROUS_EXTENSIONS.include?(extension)
|
||||
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||
end
|
||||
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open,
|
||||
filename: file.original_filename,
|
||||
@@ -165,6 +153,21 @@ module Submitters
|
||||
preferences
|
||||
end
|
||||
|
||||
def fetch_values_for_delegate(submitter)
|
||||
fields = submitter.submission.template_fields || submitter.template.fields
|
||||
default_values = submitter.preferences['default_values'] || {}
|
||||
|
||||
field_uuids = fields.filter_map do |field|
|
||||
next if field['submitter_uuid'] != submitter.uuid
|
||||
next unless field['type'].in?(%w[signature phone verification kba initials])
|
||||
next if default_values[field['uuid']].present?
|
||||
|
||||
field['uuid']
|
||||
end
|
||||
|
||||
submitter.values.except(*field_uuids)
|
||||
end
|
||||
|
||||
def send_signature_requests(submitters, delay_seconds: nil)
|
||||
submitters.each_with_index do |submitter, index|
|
||||
next if submitter.email.blank?
|
||||
@@ -238,7 +241,7 @@ module Submitters
|
||||
I18n.l(completed_at.in_time_zone(submitter.account.timezone), format: :short)
|
||||
end
|
||||
|
||||
"#{filename}.#{blob.filename.extension}"
|
||||
"#{filename}.#{blob.filename.extension}".tr('/', '-')
|
||||
end
|
||||
|
||||
def send_shared_link_email_verification_code(submitter, request:)
|
||||
|
||||
@@ -247,7 +247,7 @@ module Submitters
|
||||
|
||||
detected_extensions = Marcel::TYPE_EXTS[mime_type].to_a.map(&:downcase)
|
||||
|
||||
if detected_extensions.any? { |e| Submitters::DANGEROUS_EXTENSIONS.include?(e) }
|
||||
if detected_extensions.any? { |e| FilenameUtils::DANGEROUS_EXTENSIONS.include?(e) }
|
||||
raise InvalidDefaultValue, "File type '.#{detected_extensions.first}' is not allowed."
|
||||
end
|
||||
|
||||
@@ -276,11 +276,9 @@ module Submitters
|
||||
|
||||
def find_or_create_blob_from_url(account, url)
|
||||
filename = Addressable::URI.parse(url).path.split('/').last.to_s
|
||||
extension = File.extname(filename).delete_prefix('.').downcase
|
||||
extension = FilenameUtils.dangerous_extension(filename)
|
||||
|
||||
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
||||
raise InvalidDefaultValue, "File type '.#{extension}' is not allowed."
|
||||
end
|
||||
raise InvalidDefaultValue, "File type '.#{extension}' is not allowed." if extension
|
||||
|
||||
cache_key = [account.id, url].join(':')
|
||||
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
||||
|
||||
@@ -490,7 +490,7 @@ module Submitters
|
||||
|
||||
submission.submitters.create!(uuid: s['uuid'], email:, phone:, 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: s['uuid'] })
|
||||
|
||||
is_invited = true
|
||||
end
|
||||
|
||||
@@ -76,7 +76,7 @@ module Templates
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(document_data),
|
||||
filename: file.original_filename,
|
||||
filename: file.original_filename.tr('/', '-'),
|
||||
metadata: {
|
||||
**metadata,
|
||||
identified: file.content_type == PDF_CONTENT_TYPE,
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
module Users
|
||||
module_function
|
||||
|
||||
def send_emails?(user)
|
||||
!user.archived_at?
|
||||
end
|
||||
|
||||
def generate_csv(users)
|
||||
headers = %w[email first_name last_name role current_sign_in_at last_sign_in_at updated_at created_at]
|
||||
|
||||
|
||||
+31
-33
@@ -10,20 +10,22 @@ module VerifyPdfSignature
|
||||
module_function
|
||||
|
||||
def call(io, trusted_certs)
|
||||
Pdfium::Document.open_io(io) do |document|
|
||||
signatures = document.signatures.select { |e| e.byte_range.any?(&:positive?) && e.contents.present? }
|
||||
Pdfium.with_instance do
|
||||
Pdfium::Document.open_io(io) do |document|
|
||||
signatures = document.signatures.select { |e| e.byte_range.any?(&:positive?) && e.contents.present? }
|
||||
|
||||
next [] if signatures.blank?
|
||||
next [] if signatures.blank?
|
||||
|
||||
verified_signatures = signatures.select { |e| verified_signature?(e, io, trusted_certs) }
|
||||
trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) }
|
||||
last_signature = (trusted_signatures.presence || verified_signatures).max_by(&:signed_end)
|
||||
has_unsigned_changes = last_signature && unsigned_changes?(document, io, last_signature.signed_end)
|
||||
verified_signatures = signatures.select { |e| verified_signature?(e, io, trusted_certs) }
|
||||
trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) }
|
||||
last_signature = (trusted_signatures.presence || verified_signatures).max_by(&:signed_end)
|
||||
has_unsigned_changes = last_signature && unsigned_changes?(document, io, last_signature.signed_end)
|
||||
|
||||
signatures.map do |signature|
|
||||
build_signature(signature, trusted_certs,
|
||||
verified: verified_signatures.include?(signature),
|
||||
has_unsigned_changes: has_unsigned_changes && signature == last_signature)
|
||||
signatures.map do |signature|
|
||||
build_signature(signature, trusted_certs,
|
||||
verified: verified_signatures.include?(signature),
|
||||
has_unsigned_changes: has_unsigned_changes && signature == last_signature)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,11 +33,26 @@ module VerifyPdfSignature
|
||||
def verified_signature?(signature, io, trusted_certs)
|
||||
return false unless covers_signed_revision?(signature, io)
|
||||
|
||||
verify_contents(OpenSSL::PKCS7.new(signature.contents), signed_data(io, signature.byte_range), trusted_certs)
|
||||
rescue OpenSSL::PKCS7::PKCS7Error
|
||||
pkcs7 = OpenSSL::PKCS7.new(signature.contents)
|
||||
|
||||
if signature.sub_filter == 'ETSI.RFC3161'
|
||||
verify_timestamp(pkcs7, signed_data(io, signature.byte_range))
|
||||
else
|
||||
verify_contents(pkcs7, signed_data(io, signature.byte_range), trusted_certs)
|
||||
end
|
||||
rescue OpenSSL::PKCS7::PKCS7Error, OpenSSL::Timestamp::TimestampError
|
||||
false
|
||||
end
|
||||
|
||||
def verify_timestamp(pkcs7, signed_data)
|
||||
return false unless pkcs7.verify(pkcs7.certificates, OpenSSL::X509::Store.new, nil,
|
||||
OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::BINARY)
|
||||
|
||||
token_info = OpenSSL::Timestamp::TokenInfo.new(pkcs7.data)
|
||||
|
||||
token_info.message_imprint == OpenSSL::Digest.digest(token_info.algorithm, signed_data)
|
||||
end
|
||||
|
||||
def build_signature(signature, trusted_certs, verified:, has_unsigned_changes:)
|
||||
pkcs7 = OpenSSL::PKCS7.new(signature.contents)
|
||||
|
||||
@@ -173,29 +190,10 @@ module VerifyPdfSignature
|
||||
io.seek(0)
|
||||
|
||||
Pdfium::Document.open_bytes(io.read(signed_end)) do |signed_document|
|
||||
next true unless signed_document.valid_cross_reference_table?
|
||||
|
||||
serialized_document(signed_document) != serialized_document(document)
|
||||
!document.only_dss_changes?(signed_document)
|
||||
end
|
||||
end
|
||||
|
||||
def serialized_document(document)
|
||||
pages = (0...document.page_count).map do |index|
|
||||
page = document.get_page(index)
|
||||
objects = page.objects.map { |object| [*object.to_a, image_digest(page, object)] }
|
||||
|
||||
[page.rotation, objects, page.annotations, page.text]
|
||||
end
|
||||
|
||||
[pages, document.bookmarks]
|
||||
end
|
||||
|
||||
def image_digest(page, object)
|
||||
return unless object.image?
|
||||
|
||||
Digest::SHA256.hexdigest(page.extract_image_bitmap(object.object_ptr)[:data])
|
||||
end
|
||||
|
||||
def signed_data(io, byte_range)
|
||||
byte_range.each_slice(2).map do |offset, length|
|
||||
io.seek(offset)
|
||||
|
||||
Reference in new issue
Block a user