Merge from docusealco/wip

master 3.2.6
Alex Turchyn 17 hours ago committed by GitHub
commit 47c090e1f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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

@ -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

@ -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),

@ -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,

@ -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

@ -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

@ -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 %>

@ -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>

@ -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? %>

@ -12,7 +12,7 @@
<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>

@ -12,7 +12,7 @@
<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>

@ -12,7 +12,7 @@
<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>

@ -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 %>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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

@ -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
autorot ? image.autorot : image LoadIco.call(data)
end when BMP_REGEXP
LoadBmp.call(data)
else
raise UnsupportedFormat, content_type.to_s
end
autorot ? image.autorot : image
end end
def blank?(image) def blank?(image)

@ -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

@ -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,

@ -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)

@ -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) }
trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) } verified_signatures = signatures.select { |e| verified_signature?(e, io, trusted_certs) }
last_signature = (trusted_signatures.presence || verified_signatures).max_by(&:signed_end) trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) }
has_unsigned_changes = last_signature && unsigned_changes?(document, io, last_signature.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)
signatures.map do |signature|
build_signature(signature, trusted_certs, signatures.map do |signature|
verified: verified_signatures.include?(signature), build_signature(signature, trusted_certs,
has_unsigned_changes: has_unsigned_changes && signature == last_signature) verified: verified_signatures.include?(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,27 +190,8 @@ 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
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 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 end
def signed_data(io, byte_range) def signed_data(io, byte_range)

Loading…
Cancel
Save