Compare commits

..
11 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
33 changed files with 126 additions and 108 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
@@ -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),
+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,
+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
@@ -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? %>
@@ -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>
+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 %>
+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
@@ -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>
+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
+2 -14
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,
+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)
+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)