Compare commits

...
27 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
62 changed files with 216 additions and 157 deletions

No files matched your search

+1 -5
View File
@@ -152,11 +152,7 @@ jobs:
bundle config path vendor/bundle bundle config path vendor/bundle
bundle install --jobs 4 --retry 4 bundle install --jobs 4 --retry 4
yarn install yarn install
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip" wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-musl-$(uname -m).zip"
case "$(uname -m)" in
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;;
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;;
esac | sha256sum -c -
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
rm -rf pdfium-linux.zip /tmp/pdfium-linux rm -rf pdfium-linux.zip /tmp/pdfium-linux
+3 -3
View File
@@ -9,10 +9,10 @@ RUN apk --no-cache add wget unzip && \
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \ wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \ wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \ wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip" && \ wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260920/pdfium-musl-$(uname -m).zip" && \
case "$(uname -m)" in \ case "$(uname -m)" in \
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;; \ x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;; \ aarch64) echo "73b72ed79b9bfdd494dee7e863482c24971a8d4b11f6cd1289aa170d31304a1f pdfium-linux.zip" ;; \
esac | sha256sum -c - && \ esac | sha256sum -c - && \
mkdir -p /pdfium-linux && \ mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /pdfium-linux unzip -q pdfium-linux.zip -d /pdfium-linux
+2 -1
View File
@@ -36,7 +36,7 @@ gem 'rotp'
gem 'rouge', require: false gem 'rouge', require: false
gem 'rqrcode', require: false gem 'rqrcode', require: false
gem 'ruby-vips' gem 'ruby-vips'
gem 'rubyXL', require: false gem 'rubyzip', require: false
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq' gem 'sidekiq'
gem 'sqlite3', require: false gem 'sqlite3', require: false
@@ -45,6 +45,7 @@ gem 'trilogy', require: false
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
gem 'xlsxtream', require: false
group :development, :test do group :development, :test do
gem 'better_html' gem 'better_html'
+5 -4
View File
@@ -497,9 +497,6 @@ GEM
ruby-vips (2.3.0) ruby-vips (2.3.0)
ffi (~> 1.12) ffi (~> 1.12)
logger logger
rubyXL (3.4.35)
nokogiri (>= 1.10.8)
rubyzip (>= 3.2.2)
rubyzip (3.2.2) rubyzip (3.2.2)
securerandom (0.4.1) securerandom (0.4.1)
semantic_range (3.1.1) semantic_range (3.1.1)
@@ -575,10 +572,13 @@ GEM
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yaml (0.4.0) yaml (0.4.0)
zeitwerk (2.8.2) zeitwerk (2.8.2)
zip_kit (6.3.4)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@@ -638,7 +638,7 @@ DEPENDENCIES
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
ruby-vips ruby-vips
rubyXL rubyzip
shakapacker shakapacker
sidekiq sidekiq
simplecov simplecov
@@ -650,6 +650,7 @@ DEPENDENCIES
tzinfo-data tzinfo-data
web-console web-console
webmock webmock
xlsxtream
RUBY VERSION RUBY VERSION
ruby 4.0.5 ruby 4.0.5
@@ -23,7 +23,7 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid) blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase) if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar) Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content return head :unprocessable_content
@@ -19,7 +19,7 @@ module Api
return head :not_found unless blob return head :not_found unless blob
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase) if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar) Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content return head :unprocessable_content
@@ -46,6 +46,8 @@ module Mcp
authorize!(:create, @template) authorize!(:create, @template)
Templates.maybe_assign_access(@template)
if mcp_params['url'].present? if mcp_params['url'].present?
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
@@ -26,8 +26,9 @@ class PreviewDocumentPageController < ActionController::API
@template = attachment.record @template = attachment.record
preview_image = attachment.preview_images.joins(:blob) preview_image =
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] }) attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id].to_i}.png", "#{params[:id].to_i}.jpg"] })
if preview_image if preview_image
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),
@@ -26,7 +26,8 @@ class SubmitFormDeclineController < ApplicationController
user = @submitter.submission.created_by_user || @submitter.template.author user = @submitter.submission.created_by_user || @submitter.template.author
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false if Users.send_emails?(user) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(@submitter, user).deliver_later! SubmitterMailer.declined_email(@submitter, user).deliver_later!
end end
@@ -32,7 +32,8 @@ class SubmitFormDelegateController < ApplicationController
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request, SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
{ old_email: @submitter.email, email: }) { old_email: @submitter.email, email: })
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14)) @submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14),
values: Submitters.fetch_values_for_delegate(@submitter))
end end
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id) SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@@ -23,7 +23,7 @@ class SubmitFormInviteController < ApplicationController
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id) @submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid }) SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: attrs[:uuid] })
end end
@submitter.submission.update!(submitters_order: :preserved) @submitter.submission.update!(submitters_order: :preserved)
@@ -63,7 +63,7 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url], validate: true).body) tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
tempfile.rewind tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present? filename = URI.decode_www_form_component(params[:filename]).tr('/', '-') if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url])) filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
+2 -4
View File
@@ -11,11 +11,9 @@ class UserInitialsController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank? return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase extension = FilenameUtils.dangerous_extension(file.original_filename)
if Submitters::DANGEROUS_EXTENSIONS.include?(extension) raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
@@ -11,11 +11,9 @@ class UserSignaturesController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank? return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase extension = FilenameUtils.dangerous_extension(file.original_filename)
if Submitters::DANGEROUS_EXTENSIONS.include?(extension) raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
+9 -1
View File
@@ -3,11 +3,19 @@
class WebhookSecretController < ApplicationController class WebhookSecretController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false load_and_authorize_resource :webhook_url, parent: false
HEADER_NAME_REGEXP = /\A[\w-]+\z/
def show; end def show; end
def update def update
key = webhook_secret_params[:key]
if key.present? && !HEADER_NAME_REGEXP.match?(key)
return redirect_back(fallback_location: settings_webhook_path(@webhook_url), alert: I18n.t('unable_to_save'))
end
@webhook_url.update!(secret: { @webhook_url.update!(secret: {
webhook_secret_params[:key] => webhook_secret_params[:value] key => webhook_secret_params[:value]
}.compact_blank) }.compact_blank)
redirect_back(fallback_location: settings_webhook_path(@webhook_url), redirect_back(fallback_location: settings_webhook_path(@webhook_url),
+1 -1
View File
@@ -353,7 +353,7 @@ function buildExtensions ({ Node, Mark, Extension, Plugin, Decoration, Decoratio
const buildDecorations = (doc) => { const buildDecorations = (doc) => {
const decorations = [] const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => { doc.descendants((node, pos) => {
if (!node.isText) return if (!node.isText) return
+1 -1
View File
@@ -150,7 +150,7 @@ export default actionable(targetable(class extends HTMLElement {
const buildDecorations = (doc) => { const buildDecorations = (doc) => {
const decorations = [] const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => { doc.descendants((node, pos) => {
if (!node.isText) return if (!node.isText) return
@@ -261,7 +261,7 @@ export default {
} }
}, },
resendCode () { resendCode () {
if (this.codeSentAt && Date.now() - this.codeSentAt < 15000) { if (this.codeSentAt && Date.now() - this.codeSentAt < 30000) {
this.startResendCodeCountdown() this.startResendCodeCountdown()
} else { } else {
this.isResendLoading = true this.isResendLoading = true
@@ -274,7 +274,7 @@ export default {
} }
}, },
startResendCodeCountdown () { startResendCodeCountdown () {
this.resendCodeCountdown = 15 - parseInt((Date.now() - this.codeSentAt) / 1000) this.resendCodeCountdown = 30 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.interval = setInterval(() => { this.interval = setInterval(() => {
this.resendCodeCountdown-- this.resendCodeCountdown--
+16 -2
View File
@@ -2371,7 +2371,6 @@ export default {
const areaCopy = JSON.parse(JSON.stringify(area)) const areaCopy = JSON.parse(JSON.stringify(area))
delete fieldCopy.areas delete fieldCopy.areas
delete fieldCopy.submitter_uuid
areaCopy.relativeX = area.x - minX areaCopy.relativeX = area.x - minX
areaCopy.relativeY = area.y - minY areaCopy.relativeY = area.y - minY
@@ -2381,6 +2380,7 @@ export default {
const clipboardData = { const clipboardData = {
items, items,
submitters: this.template.submitters.map((submitter) => ({ uuid: submitter.uuid, name: submitter.name })),
templateId: this.template.id, templateId: this.template.id,
timestamp: Date.now(), timestamp: Date.now(),
isGroup: true isGroup: true
@@ -2491,6 +2491,20 @@ export default {
const fieldUuidIndex = {} const fieldUuidIndex = {}
const fieldOptionsMap = {} const fieldOptionsMap = {}
const submitterUuidsMap = {}
const copiedSubmitterUuids = [...new Set(data.items.map((item) => item.field.submitter_uuid).filter(Boolean))]
if (copiedSubmitterUuids.length > 1) {
copiedSubmitterUuids.forEach((uuid) => {
const name = data.submitters?.find((submitter) => submitter.uuid === uuid)?.name
const submitter = this.template.submitters.find((s) => s.uuid === uuid) ||
(name && this.template.submitters.find((s) => s.name.toLowerCase() === name.toLowerCase()))
submitterUuidsMap[uuid] = (submitter || this.selectedSubmitter).uuid
})
}
data.items.forEach((item) => { data.items.forEach((item) => {
const field = JSON.parse(JSON.stringify(item.field)) const field = JSON.parse(JSON.stringify(item.field))
@@ -2515,7 +2529,7 @@ export default {
const newField = fieldUuidIndex[field.uuid] || { const newField = fieldUuidIndex[field.uuid] || {
...field, ...field,
uuid: v4(), uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid, submitter_uuid: submitterUuidsMap[field.submitter_uuid] || this.selectedSubmitter.uuid,
areas: [] areas: []
} }
+1 -1
View File
@@ -133,7 +133,7 @@ class ProcessSubmitterCompletionJob
user_submitter = submission.submitters.find { |s| s.email == user.email } user_submitter = submission.submitters.find { |s| s.email == user.email }
is_sent_to_user = is_sent_to_user =
if user.role != 'integration' && if user.role != 'integration' && Users.send_emails?(user) &&
(!user_submitter || user_submitter.preferences['send_email'] == false || !is_copy_email_enabled) && (!user_submitter || user_submitter.preferences['send_email'] == false || !is_copy_email_enabled) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
SubmitterMailer.completed_email(submitter, user).deliver_later! SubmitterMailer.completed_email(submitter, user).deliver_later!
+1 -1
View File
@@ -90,7 +90,7 @@ class Submitter < ApplicationRecord
def friendly_name def friendly_name
if name.present? && email.present? && email.exclude?(',') if name.present? && email.present? && email.exclude?(',')
%("#{name.delete('"')}" <#{email}>) %("#{name.delete('"').squish}" <#{email}>)
else else
email email
end end
@@ -3,7 +3,7 @@
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %> <%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %>
<span class="flex items-center"> <span class="flex items-center">
<span class="whitespace-nowrap"><%= t('docuseal_trusted_signature') %></span> <span class="whitespace-nowrap"><%= t('docuseal_trusted_signature') %></span>
<div class="tooltip tooltip-top ml-1 shrink-0" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>"> <div class="tooltip tooltip-top ml-1 shrink-0 hidden md:flex" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1 shrink-0') %> <%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1 shrink-0') %>
</div> </div>
</span> </span>
@@ -14,7 +14,7 @@
</a> </a>
</td> </td>
<td> <td>
<div class="tooltip" data-tip="<%= t('unlock_with_docuseal_pro') %>"> <div class="md:tooltip" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= button_to settings_esign_path, method: :put, params: { name: Docuseal::AATL_CERT_NAME }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: t('make_default'), disabled: true do %> <%= button_to settings_esign_path, method: :put, params: { name: Docuseal::AATL_CERT_NAME }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: t('make_default'), disabled: true do %>
<%= t('make_default') %> <%= t('make_default') %>
<% end %> <% end %>
+1 -1
View File
@@ -120,7 +120,7 @@
<span> <span>
<%= t('timeserver_url') %> <%= t('timeserver_url') %>
</span> </span>
<span class="tooltip" data-tip="<%= t('url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures') %>"> <span class="tooltip hidden md:flex" data-tip="<%= t('url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</span> </span>
+4 -6
View File
@@ -6,12 +6,10 @@
<%= t('mcp_server') %> <%= t('mcp_server') %>
</h1> </h1>
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center"> <div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
<div class="tooltip"> <%= link_to new_settings_mcp_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= link_to new_settings_mcp_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %> <%= svg_icon('plus', class: 'w-6 h-6') %>
<%= svg_icon('plus', class: 'w-6 h-6') %> <span><%= t('new_token') %></span>
<span><%= t('new_token') %></span> <% end %>
<% end %>
</div>
</div> </div>
</div> </div>
<% if @mcp_token.present? %> <% if @mcp_token.present? %>
@@ -6,13 +6,13 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip 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') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
@@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_BUTTON_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_BUTTON_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %> <%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
<div class="form-control"> <div class="form-control">
@@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_MESSAGE_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_MESSAGE_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:title, :body).new(*(f.object.value || {}).values_at('title', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:title, :body).new(*(f.object.value || {}).values_at('title', 'body')) do |ff| %>
<div class="form-control"> <div class="form-control">
@@ -7,7 +7,7 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <div class="form-control">
<autoresize-textarea> <autoresize-textarea>
@@ -6,13 +6,13 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip 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') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
@@ -6,13 +6,13 @@
</div> </div>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip 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') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
+1 -1
View File
@@ -36,7 +36,7 @@
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %> <%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</span> </span>
<% if (device = DetectBrowserDevice.call(event.data['ua'])) %> <% if (device = DetectBrowserDevice.call(event.data['ua'])) %>
<span class="tooltip tooltip-top" data-tip="<%= t(device) %>"> <span class="md:tooltip tooltip-top" data-tip="<%= t(device) %>">
<%= svg_icon("device_#{device}", class: 'w-4 h-4') %> <%= svg_icon("device_#{device}", class: 'w-4 h-4') %>
</span> </span>
<% end %> <% end %>
@@ -7,7 +7,7 @@
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %> <%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: last_submission&.submitters_order.in?(['preserved', nil]) %> <%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: last_submission&.submitters_order.in?(['preserved', nil]) %>
<span class="label"><%= t('preserve_order') %></span> <span class="label"><%= t('preserve_order') %></span>
<span class="tooltip" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>"> <span class="tooltip hidden md:flex" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
<% end %> <% end %>
@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<% view_archived_html = capture do %> <% view_archived_html = capture do %>
<% if can?(:manage, :countless) || current_account.submissions.where.not(archived_at: nil).exists? %> <% if can?(:manage, :countless) || current_account.submissions.archived.exists? || current_account.templates.where(Submission.where(Submission.arel_table[:template_id].eq(::Template.arel_table[:id])).arel.exists).archived.exists? %>
<div> <div>
<a href="<%= submissions_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a> <a href="<%= submissions_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
</div> </div>
+2 -2
View File
@@ -56,7 +56,7 @@
<div class="mockup-code overflow-hidden pb-0 mt-4"> <div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex"> <span class="top-0 right-0 absolute flex">
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %> <%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script> <clipboard-copy data-text="<script src=&quot;<%= Docuseal.multitenant? ? "#{Docuseal::CDN_URL}/js/form.js" : embed_script_url(filename: 'form.js') %>&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form> <docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
"> ">
@@ -78,7 +78,7 @@
</clipboard-copy> </clipboard-copy>
</span> </span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span> <pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal.multitenant? ? "#{Docuseal::CDN_URL}/js/form.js" : embed_script_url(filename: 'form.js') %>"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span> <span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre> </code></pre>
+3 -3
View File
@@ -35,14 +35,14 @@
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex flex-col md:flex-row md:items-center gap-3"> <span class="flex flex-col md:flex-row md:items-center gap-3">
<% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %> <% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="md:tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t('expired') %> <%= t('expired') %>
</span> </span>
</div> </div>
<% else %> <% else %>
<a href="<%= submission_path(submission) %>" class="flex z-[1]"> <a href="<%= submission_path(submission) %>" class="flex z-[1]">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold md:tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<%= t(submitter.status) %> <%= t(submitter.status) %>
</span> </span>
</a> </a>
@@ -125,7 +125,7 @@
</span> </span>
</a> </a>
<% elsif submission.expired? %> <% elsif submission.expired? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="md:tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t('expired') %> <%= t('expired') %>
</span> </span>
+1 -1
View File
@@ -1,4 +1,4 @@
<% has_archived = can?(:manage, :countless) || current_account.templates.where.not(archived_at: nil).exists? %> <% has_archived = can?(:manage, :countless) || current_account.templates.archived.exists? %>
<% show_dropzone = params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %> <% show_dropzone = params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<dashboard-dropzone> <dashboard-dropzone>
+1 -1
View File
@@ -5,7 +5,7 @@
<%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %> <%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %>
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name"> <toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name">
<div class="flex items-center justify-between mb-2.5"> <div class="flex items-center justify-between mb-2.5">
<label for="is_root_folder" class="flex items-center tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>"> <label for="is_root_folder" class="flex items-center md:tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>">
<%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %> <%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %>
<span class="flex items-center mt-1"> <span class="flex items-center mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %> <%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %>
@@ -12,7 +12,7 @@
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %> <%= 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') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
@@ -12,7 +12,7 @@
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %> <%= 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') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
@@ -18,7 +18,7 @@
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %> <%= 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') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
@@ -12,7 +12,7 @@
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %> <%= 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') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
+1 -1
View File
@@ -10,7 +10,7 @@
<% end %> <% end %>
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %> <%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %> <% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %>
<label for="template_shared_link" class="tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>"> <label for="template_shared_link" class="md:tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>">
<span><%= t('enable_shared_link') %></span> <span><%= t('enable_shared_link') %></span>
<%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %> <%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %>
</label> </label>
+1 -1
View File
@@ -24,7 +24,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% else %> <% else %>
<div class="tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>"> <div class="md:tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>">
<%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %> <%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %> <%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span> <span><%= t('new_user') %></span>
+1 -1
View File
@@ -83,7 +83,7 @@
</div> </div>
<% if @webhook_events.present? || params[:status].present? %> <% if @webhook_events.present? || params[:status].present? %>
<div class="mt-6"> <div class="mt-6">
<h2 id="log" class="text-2xl md:text-3xl font-bold"><%= t('events_log') %></h2> <h2 id="log" class="text-2xl md:text-3xl font-bold"><%= t('event_log') %></h2>
<div class="tabs border-b mt-4"> <div class="tabs border-b mt-4">
<%= link_to t('all'), url_for(params: request.query_parameters.except('status', 'page')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> <%= link_to t('all'), url_for(params: request.query_parameters.except('status', 'page')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('succeeded'), url_for(params: request.query_parameters.except('page').merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> <%= link_to t('succeeded'), url_for(params: request.query_parameters.except('page').merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
+1 -1
View File
@@ -105,6 +105,6 @@ if ENV['REDIS_URL'].to_s.empty?
redis_password = Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}") redis_password = Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}")
ENV['REDIS_URL'] = "redis://default:#{redis_password}@0.0.0.0:6379/0" ENV['REDIS_URL'] = "redis://default:#{redis_password}@127.0.0.1:16379/0"
ENV['LOCAL_REDIS_URL'] = ENV.fetch('REDIS_URL', nil) ENV['LOCAL_REDIS_URL'] = ENV.fetch('REDIS_URL', nil)
end end
+1 -1
View File
@@ -2,7 +2,7 @@
autoload :CSV, 'csv' autoload :CSV, 'csv'
autoload :CSVSafe, 'csv-safe' autoload :CSVSafe, 'csv-safe'
autoload :RubyXL, 'rubyXL' autoload :Xlsxtream, 'xlsxtream'
autoload :Zip, 'zip' autoload :Zip, 'zip'
autoload :Numo, 'numo/narray' autoload :Numo, 'numo/narray'
autoload :OnnxRuntime, 'onnxruntime' autoload :OnnxRuntime, 'onnxruntime'
+1 -1
View File
@@ -8034,7 +8034,7 @@ nl: &nl
awaiting: Wachten awaiting: Wachten
document_id: Document-ID document_id: Document-ID
envelope_id: Envelop-ID envelope_id: Envelop-ID
event_log: Gebeurtenissenlogboek event_log: Gebeurtenislogboek
verify: Verifiëren verify: Verifiëren
testing_log_not_for_production_use: Testlogboek - Niet voor productiegebruik testing_log_not_for_production_use: Testlogboek - Niet voor productiegebruik
original_sha256: Oorspronkelijke SHA256 original_sha256: Oorspronkelijke SHA256
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
module FilenameUtils
DANGEROUS_EXTENSIONS = %w[
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
hta cpl jar app deb rpm dmg pkg mpkg dll so dylib sys
inf reg ps1 psm1 psd1 ps1xml psc1 pssc vb vba
sh bash zsh fish run out bin elf gadget workflow lnk scf
url desktop application action apk ipa xap appx
appxbundle msix msixbundle diagcab diagpkg msc ocx
drv ins isp mst paf prf shb shs slk ws wsc inf1 inf2
].freeze
DANGEROUS_EXTENSIONS_REGEXP = /\.(#{Regexp.union(DANGEROUS_EXTENSIONS).source})\W*\z/i
module_function
def dangerous_extension(filename)
ActiveStorage::Filename.wrap(filename).sanitized[DANGEROUS_EXTENSIONS_REGEXP, 1]&.downcase
end
end
+18 -8
View File
@@ -1,23 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
module ImageUtils module ImageUtils
PNG_REGEXP = %r{\Aimage/(?:png|apng|vnd\.mozilla\.apng)\z}
JPEG_REGEXP = %r{\Aimage/(?:jpeg|jpg|pjpeg)\z}
ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z} ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z}
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z} BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
UnsupportedFormat = Class.new(StandardError)
module_function module_function
def load_vips(data, content_type: nil, autorot: false) def load_vips(data, content_type: nil, autorot: false)
content_type ||= Marcel::MimeType.for(data) content_type ||= Marcel::MimeType.for(data)
if ICO_REGEXP.match?(content_type) image =
LoadIco.call(data) case content_type
elsif BMP_REGEXP.match?(content_type) when PNG_REGEXP
LoadBmp.call(data) Vips::Image.pngload_buffer(data)
else when JPEG_REGEXP
image = Vips::Image.new_from_buffer(data, '') Vips::Image.jpegload_buffer(data)
when ICO_REGEXP
LoadIco.call(data)
when BMP_REGEXP
LoadBmp.call(data)
else
raise UnsupportedFormat, content_type.to_s
end
autorot ? image.autorot : image autorot ? image.autorot : image
end
end end
def blank?(image) def blank?(image)
+10
View File
@@ -109,6 +109,7 @@ class Pdfium
attach_function :FPDF_GetLastError, [], :ulong attach_function :FPDF_GetLastError, [], :ulong
attach_function :FPDF_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong attach_function :FPDF_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong
attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int
attach_function :FPDF_HasOnlyDSSChanges, %i[FPDF_DOCUMENT FPDF_DOCUMENT], :int
attach_function :FPDF_GetSecurityHandlerRevision, [:FPDF_DOCUMENT], :int attach_function :FPDF_GetSecurityHandlerRevision, [:FPDF_DOCUMENT], :int
attach_function :FPDF_GetFormType, [:FPDF_DOCUMENT], :int attach_function :FPDF_GetFormType, [:FPDF_DOCUMENT], :int
@@ -581,6 +582,10 @@ class Pdfium
@page_rotations[page_index] ||= Pdfium.FPDFPage_GetRotationRaw(@document_ptr, page_index) @page_rotations[page_index] ||= Pdfium.FPDFPage_GetRotationRaw(@document_ptr, page_index)
end end
def reset_page_rotation(page_index)
@page_rotations.delete(page_index)
end
def encrypted? def encrypted?
Pdfium.FPDF_GetSecurityHandlerRevision(@document_ptr) >= 0 Pdfium.FPDF_GetSecurityHandlerRevision(@document_ptr) >= 0
end end
@@ -771,6 +776,10 @@ class Pdfium
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1 Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
end end
def only_dss_changes?(signed_document)
Pdfium.FPDF_HasOnlyDSSChanges(@document_ptr, signed_document.document_ptr) == 1
end
def annot_count(page_index) def annot_count(page_index)
@annot_counts[page_index] ||= Pdfium.FPDFPage_GetAnnotCountRaw(@document_ptr, page_index) @annot_counts[page_index] ||= Pdfium.FPDFPage_GetAnnotCountRaw(@document_ptr, page_index)
end end
@@ -1010,6 +1019,7 @@ class Pdfium
Pdfium.FPDFPage_SetRotation(@page_ptr, value) Pdfium.FPDFPage_SetRotation(@page_ptr, value)
@document.reset_page_size(@page_index) @document.reset_page_size(@page_index)
@document.reset_page_rotation(@page_index)
@rotation = value @rotation = value
end end
+1
View File
@@ -54,6 +54,7 @@ Puma::Plugin.create do
Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty? Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty?
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"), exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
'--bind', '127.0.0.1', '--port', '16379',
'--loglevel', 'warning') '--loglevel', 'warning')
end end
end end
+1 -1
View File
@@ -62,7 +62,7 @@ module Submissions
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - " \ io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - " \
"#{submission.name || submission.template&.name}.pdf" "#{submission.name || submission.template&.name}.pdf".tr('/', '-')
), ),
name: 'audit_trail', name: 'audit_trail',
record: submission record: submission
@@ -37,7 +37,7 @@ module Submissions
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{submission.name || submission.template&.name}.pdf" io: io.tap(&:rewind), filename: "#{submission.name || submission.template&.name}.pdf".tr('/', '-')
), ),
name: with_audit ? 'combined_document' : 'merged_document', name: with_audit ? 'combined_document' : 'merged_document',
record: submission record: submission
+8 -13
View File
@@ -19,22 +19,19 @@ module Submissions
end end
def rows_to_xlsx(rows) 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 = 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| io = StringIO.new
extract_columns(row, headers).each_with_index do |value, column_index|
worksheet.add_cell(row_index, column_index, value) 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
end end
workbook.stream.string io.string
end end
def rows_to_csv(rows) def rows_to_csv(rows)
@@ -150,8 +147,6 @@ module Submissions
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
end end
elsif submitter_value == true || submitter_value == false
submitter_value.to_s
else else
submitter_value submitter_value
end end
@@ -71,7 +71,7 @@ module Submissions
submission:, submission:,
values_hash:, values_hash:,
name: 'preview_merged_document', name: 'preview_merged_document',
filename: "#{submission.name || template&.name}.pdf" filename: "#{submission.name || template&.name}.pdf".tr('/', '-')
) )
ApplicationRecord.no_touching { attachment.save! } ApplicationRecord.no_touching { attachment.save! }
@@ -115,7 +115,7 @@ module Submissions
submitter:, submitter:,
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)), uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
values_hash:, values_hash:,
filename: "#{submission.name || template&.name}.pdf" filename: "#{submission.name || template&.name}.pdf".tr('/', '-')
) )
ApplicationRecord.no_touching do ApplicationRecord.no_touching do
@@ -814,7 +814,7 @@ module Submissions
end end
ActiveStorage::Attachment.new( 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, metadata: { original_uuid: uuid,
analyzed: true, analyzed: true,
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) }, sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
+18 -15
View File
@@ -16,16 +16,6 @@ module Submitters
MaliciousFileExtension = Class.new(StandardError) MaliciousFileExtension = Class.new(StandardError)
ParamsError = 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 FILES_TTL = 5.minutes
module_function module_function
@@ -127,11 +117,9 @@ module Submitters
def create_attachment!(submitter, file, metadata: {}) def create_attachment!(submitter, file, metadata: {})
raise ParamsError, 'file param is missing' if file.blank? 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." if extension
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open, blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open,
filename: file.original_filename, filename: file.original_filename,
@@ -165,6 +153,21 @@ module Submitters
preferences preferences
end 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) def send_signature_requests(submitters, delay_seconds: nil)
submitters.each_with_index do |submitter, index| submitters.each_with_index do |submitter, index|
next if submitter.email.blank? next if submitter.email.blank?
@@ -238,7 +241,7 @@ module Submitters
I18n.l(completed_at.in_time_zone(submitter.account.timezone), format: :short) I18n.l(completed_at.in_time_zone(submitter.account.timezone), format: :short)
end end
"#{filename}.#{blob.filename.extension}" "#{filename}.#{blob.filename.extension}".tr('/', '-')
end end
def send_shared_link_email_verification_code(submitter, request:) def send_shared_link_email_verification_code(submitter, request:)
+3 -5
View File
@@ -247,7 +247,7 @@ module Submitters
detected_extensions = Marcel::TYPE_EXTS[mime_type].to_a.map(&:downcase) 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." raise InvalidDefaultValue, "File type '.#{detected_extensions.first}' is not allowed."
end end
@@ -276,11 +276,9 @@ module Submitters
def find_or_create_blob_from_url(account, url) def find_or_create_blob_from_url(account, url)
filename = Addressable::URI.parse(url).path.split('/').last.to_s 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." if extension
raise InvalidDefaultValue, "File type '.#{extension}' is not allowed."
end
cache_key = [account.id, url].join(':') cache_key = [account.id, url].join(':')
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key) checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
+1 -1
View File
@@ -490,7 +490,7 @@ module Submitters
submission.submitters.create!(uuid: s['uuid'], email:, phone:, account_id: submitter.account_id) 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 is_invited = true
end end
+1 -1
View File
@@ -76,7 +76,7 @@ module Templates
blob = ActiveStorage::Blob.create_and_upload!( blob = ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(document_data), io: StringIO.new(document_data),
filename: file.original_filename, filename: file.original_filename.tr('/', '-'),
metadata: { metadata: {
**metadata, **metadata,
identified: file.content_type == PDF_CONTENT_TYPE, identified: file.content_type == PDF_CONTENT_TYPE,
+4
View File
@@ -3,6 +3,10 @@
module Users module Users
module_function module_function
def send_emails?(user)
!user.archived_at?
end
def generate_csv(users) def generate_csv(users)
headers = %w[email first_name last_name role current_sign_in_at last_sign_in_at updated_at created_at] headers = %w[email first_name last_name role current_sign_in_at last_sign_in_at updated_at created_at]
+31 -33
View File
@@ -10,20 +10,22 @@ module VerifyPdfSignature
module_function module_function
def call(io, trusted_certs) def call(io, trusted_certs)
Pdfium::Document.open_io(io) do |document| Pdfium.with_instance do
signatures = document.signatures.select { |e| e.byte_range.any?(&:positive?) && e.contents.present? } 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) } verified_signatures = signatures.select { |e| verified_signature?(e, io, trusted_certs) }
trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) } trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) }
last_signature = (trusted_signatures.presence || verified_signatures).max_by(&:signed_end) last_signature = (trusted_signatures.presence || verified_signatures).max_by(&:signed_end)
has_unsigned_changes = last_signature && unsigned_changes?(document, io, last_signature.signed_end) has_unsigned_changes = last_signature && unsigned_changes?(document, io, last_signature.signed_end)
signatures.map do |signature| signatures.map do |signature|
build_signature(signature, trusted_certs, build_signature(signature, trusted_certs,
verified: verified_signatures.include?(signature), verified: verified_signatures.include?(signature),
has_unsigned_changes: has_unsigned_changes && signature == last_signature) has_unsigned_changes: has_unsigned_changes && signature == last_signature)
end
end end
end end
end end
@@ -31,11 +33,26 @@ module VerifyPdfSignature
def verified_signature?(signature, io, trusted_certs) def verified_signature?(signature, io, trusted_certs)
return false unless covers_signed_revision?(signature, io) return false unless covers_signed_revision?(signature, io)
verify_contents(OpenSSL::PKCS7.new(signature.contents), signed_data(io, signature.byte_range), trusted_certs) pkcs7 = OpenSSL::PKCS7.new(signature.contents)
rescue OpenSSL::PKCS7::PKCS7Error
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 false
end 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:) def build_signature(signature, trusted_certs, verified:, has_unsigned_changes:)
pkcs7 = OpenSSL::PKCS7.new(signature.contents) pkcs7 = OpenSSL::PKCS7.new(signature.contents)
@@ -173,29 +190,10 @@ module VerifyPdfSignature
io.seek(0) io.seek(0)
Pdfium::Document.open_bytes(io.read(signed_end)) do |signed_document| Pdfium::Document.open_bytes(io.read(signed_end)) do |signed_document|
next true unless signed_document.valid_cross_reference_table? !document.only_dss_changes?(signed_document)
serialized_document(signed_document) != serialized_document(document)
end end
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) def signed_data(io, byte_range)
byte_range.each_slice(2).map do |offset, length| byte_range.each_slice(2).map do |offset, length|
io.seek(offset) io.seek(offset)