mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 06:42:32 -05:00
Compare commits
21
Commits
8f8f69f0bc
..
3.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a1835c00 | ||
|
|
c35407d7a9 | ||
|
|
e55ccc0b76 | ||
|
|
b8d00870b4 | ||
|
|
5413fbc248 | ||
|
|
2d2eb7ce4d | ||
|
|
ed2ddbf74e | ||
|
|
b99e5d1262 | ||
|
|
87f6878eda | ||
|
|
99b815eb4b | ||
|
|
e40a29b9ab | ||
|
|
151890bb72 | ||
|
|
78edd87cc1 | ||
|
|
88c99a2b35 | ||
|
|
282b8cfa73 | ||
|
|
e9888c7466 | ||
|
|
0647befb3a | ||
|
|
85aa3d0451 | ||
|
|
6d2b3bb654 | ||
|
|
b394854e9d | ||
|
|
2e586e930e |
No files matched your search
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 4.0.5
|
||||
ruby-version: 4.0.9
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 4.0.5
|
||||
ruby-version: 4.0.9
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 4.0.5
|
||||
ruby-version: 4.0.9
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
container:
|
||||
image: ruby:4.0.5-alpine
|
||||
image: ruby:4.0.9-alpine
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
||||
+5
-4
@@ -1,4 +1,4 @@
|
||||
FROM ruby:4.0.5-alpine AS download
|
||||
FROM ruby:4.0.9-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN apk --no-cache add wget unzip && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
unzip -q pdfium-linux.zip -d /pdfium-linux
|
||||
|
||||
FROM ruby:4.0.5-alpine AS webpack
|
||||
FROM ruby:4.0.9-alpine AS webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -44,7 +44,7 @@ COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:4.0.5-alpine AS app
|
||||
FROM ruby:4.0.9-alpine AS app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
@@ -52,7 +52,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
|
||||
RUN apk upgrade --no-cache ca-certificates-bundle && \
|
||||
apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
|
||||
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
|
||||
|
||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '4.0.5'
|
||||
ruby '4.0.9'
|
||||
|
||||
gem 'addressable'
|
||||
gem 'arabic-letter-connector', require: false
|
||||
|
||||
+6
-6
@@ -572,15 +572,15 @@ GEM
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wordcraft (0.8.0-aarch64-linux-gnu)
|
||||
wordcraft (0.11.0-aarch64-linux-gnu)
|
||||
ffi (>= 1.15)
|
||||
wordcraft (0.8.0-aarch64-linux-musl)
|
||||
wordcraft (0.11.0-aarch64-linux-musl)
|
||||
ffi (>= 1.15)
|
||||
wordcraft (0.8.0-arm64-darwin)
|
||||
wordcraft (0.11.0-arm64-darwin)
|
||||
ffi (>= 1.15)
|
||||
wordcraft (0.8.0-x86_64-linux-gnu)
|
||||
wordcraft (0.11.0-x86_64-linux-gnu)
|
||||
ffi (>= 1.15)
|
||||
wordcraft (0.8.0-x86_64-linux-musl)
|
||||
wordcraft (0.11.0-x86_64-linux-musl)
|
||||
ffi (>= 1.15)
|
||||
xlsxtream (3.1.0)
|
||||
zip_kit (>= 6.2, < 7)
|
||||
@@ -664,7 +664,7 @@ DEPENDENCIES
|
||||
xlsxtream
|
||||
|
||||
RUBY VERSION
|
||||
ruby 4.0.5
|
||||
ruby 4.0.9
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.3
|
||||
@@ -1,74 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class AttachmentsController < ActionController::API
|
||||
include ActionController::Cookies
|
||||
include ActiveStorage::SetCurrent
|
||||
|
||||
COOKIE_STORE_LIMIT = 10
|
||||
|
||||
def create
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
unless can_upload?(@submitter)
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
file = params[:file]
|
||||
|
||||
if params[:type].in?(%w[initials signature])
|
||||
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
|
||||
|
||||
if ImageUtils.blank?(image)
|
||||
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if ImageUtils.error?(image)
|
||||
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
||||
|
||||
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||
cookies.permanent.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
rescue Submitters::MaliciousFileExtension => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def can_upload?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
end
|
||||
|
||||
def build_new_cookie_signatures_json(submitter, attachment)
|
||||
values =
|
||||
begin
|
||||
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
values[submitter.email] = attachment.uuid
|
||||
|
||||
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
|
||||
|
||||
values.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmitterEmailClicksController < ActionController::API
|
||||
def create
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||
end
|
||||
|
||||
render json: {}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -179,10 +179,7 @@ module Api
|
||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
|
||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||
|
||||
if params[:template_id].present?
|
||||
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||
end
|
||||
submitters = submitters.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||
|
||||
maybe_filter_by_completed_at(submitters, params)
|
||||
end
|
||||
|
||||
@@ -228,6 +228,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
template,
|
||||
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
|
||||
autosave: this.dataset.autosave !== 'false',
|
||||
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
||||
backgroundColor: '#faf7f5',
|
||||
locale: this.dataset.locale,
|
||||
|
||||
@@ -12,7 +12,14 @@ export default class extends HTMLElement {
|
||||
toggle = (event) => {
|
||||
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
|
||||
|
||||
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field)
|
||||
if (event?.type === 'focusin' && !this.isKeepSendUsed && this.isFocusFromName(event)) {
|
||||
this.isKeepSendUsed = true
|
||||
this.keepSendInput = event.target
|
||||
} else if (event?.type === 'focusout' && event.target === this.keepSendInput) {
|
||||
this.keepSendInput = null
|
||||
}
|
||||
|
||||
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) && !this.keepSendInput?.matches(this.fieldSelector(field))
|
||||
|
||||
const isSend = (this.isChecked('send_email') && !isNameOnly('email')) ||
|
||||
(this.isChecked('send_sms') && !isNameOnly('phone'))
|
||||
@@ -29,12 +36,17 @@ export default class extends HTMLElement {
|
||||
return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim())
|
||||
}
|
||||
|
||||
isFocusFromName ({ target, relatedTarget }) {
|
||||
return !!relatedTarget?.matches(this.fieldSelector('name')) && !!relatedTarget.value.trim() &&
|
||||
['email', 'phone'].some((field) => target.matches(this.fieldSelector(field)))
|
||||
}
|
||||
|
||||
fieldSelector (field) {
|
||||
return `[name$="[][${field}]"]`
|
||||
}
|
||||
|
||||
get events () {
|
||||
return ['change', 'input', 'linked-input.update']
|
||||
return ['change', 'input', 'linked-input.update', 'focusin', 'focusout']
|
||||
}
|
||||
|
||||
get form () {
|
||||
|
||||
@@ -415,9 +415,9 @@
|
||||
<a
|
||||
:href="`/templates/${template.id}/form`"
|
||||
:data-native-actions="template.submitters.length > 1 ? 'users' : null"
|
||||
data-turbo="false"
|
||||
data-turbo-prefetch="false"
|
||||
class="flex items-center justify-center space-x-2"
|
||||
@click.exact="isPreviewLoading = true"
|
||||
@click.exact="onSaveAndPreviewClick"
|
||||
>
|
||||
<IconInnerShadowTop
|
||||
v-if="isPreviewLoading"
|
||||
@@ -640,6 +640,7 @@
|
||||
:draw-field-type="drawFieldType"
|
||||
:draw-custom-field="drawCustomField"
|
||||
:draw-option="drawOption"
|
||||
:autosave="autosave"
|
||||
@update="onDynamicDocumentUpdate"
|
||||
@draw="clearDrawField"
|
||||
@add-custom-field="addCustomField"
|
||||
@@ -1379,6 +1380,7 @@ export default {
|
||||
isPreviewLoading: false,
|
||||
isLoadingBlankPage: false,
|
||||
isSaving: false,
|
||||
hasUnsavedChanges: false,
|
||||
isDetectingPageFields: false,
|
||||
detectFieldsQueue: [],
|
||||
detectingAnalyzingProgress: null,
|
||||
@@ -1662,6 +1664,11 @@ export default {
|
||||
|
||||
window.addEventListener('dragleave', this.onWindowDragLeave)
|
||||
|
||||
if (!this.autosave) {
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload)
|
||||
document.addEventListener('turbo:before-visit', this.onBeforeVisit)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (document.location.search?.includes('stripe_connect_success')) {
|
||||
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
|
||||
@@ -1685,6 +1692,11 @@ export default {
|
||||
|
||||
this.resizeObserver.disconnect()
|
||||
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
||||
|
||||
if (!this.autosave) {
|
||||
window.removeEventListener('beforeunload', this.onBeforeUnload)
|
||||
document.removeEventListener('turbo:before-visit', this.onBeforeVisit)
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
this.documentRefs = []
|
||||
@@ -2241,6 +2253,37 @@ export default {
|
||||
this.insertField(field)
|
||||
}
|
||||
},
|
||||
onSaveAndPreviewClick (event) {
|
||||
this.isPreviewLoading = true
|
||||
|
||||
if (!this.autosave) {
|
||||
event.preventDefault()
|
||||
|
||||
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
||||
|
||||
dynamicDocumentRefs.forEach((ref) => ref.update())
|
||||
|
||||
this.rebuildVariablesSchema({ disable: false })
|
||||
|
||||
Promise.all([this.save({ force: true }), ...dynamicDocumentRefs.map((ref) => ref.saveBody())]).then(() => {
|
||||
window.Turbo.visit(`/templates/${this.template.id}/form`)
|
||||
})
|
||||
}
|
||||
},
|
||||
onBeforeUnload (event) {
|
||||
if (this.hasUnsavedChanges) {
|
||||
event.preventDefault()
|
||||
|
||||
event.returnValue = ''
|
||||
|
||||
return ''
|
||||
}
|
||||
},
|
||||
onBeforeVisit (event) {
|
||||
if (this.hasUnsavedChanges && !window.confirm(this.t('close_without_saving_changes'))) {
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
closeDropdown () {
|
||||
document.activeElement.blur()
|
||||
},
|
||||
@@ -4036,6 +4079,8 @@ export default {
|
||||
}
|
||||
|
||||
if (!this.autosave && !force) {
|
||||
this.hasUnsavedChanges = true
|
||||
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
@@ -4058,6 +4103,8 @@ export default {
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(() => {
|
||||
this.hasUnsavedChanges = false
|
||||
|
||||
if (this.onSave) {
|
||||
this.onSave(this.template)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
autosave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
selectedSubmitter: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -279,7 +284,7 @@ export default {
|
||||
|
||||
this.updatedSectionEditors.set(section.id, editor)
|
||||
|
||||
this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000)
|
||||
this.saveTimer = setTimeout(() => this.autosave ? this.updateSectionsAndSave() : this.updateSections(), 1000)
|
||||
},
|
||||
loadListsStyle () {
|
||||
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
|
||||
@@ -299,7 +304,9 @@ export default {
|
||||
updateVariablesSchema () {
|
||||
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
|
||||
},
|
||||
updateSectionsAndSave () {
|
||||
updateSections () {
|
||||
delete this.saveTimer
|
||||
|
||||
this.updatedSectionEditors.forEach((editor, sectionId) => {
|
||||
const target = this.bodyDom.getElementById(sectionId)
|
||||
|
||||
@@ -315,6 +322,9 @@ export default {
|
||||
this.updateVariablesSchema()
|
||||
|
||||
this.$emit('update', this.document)
|
||||
},
|
||||
updateSectionsAndSave () {
|
||||
this.updateSections()
|
||||
|
||||
return this.saveBody()
|
||||
},
|
||||
|
||||
@@ -70,6 +70,7 @@ const en = {
|
||||
remove_condition: 'Remove condition',
|
||||
add_condition: 'Add condition',
|
||||
are_you_sure_: 'Are you sure?',
|
||||
close_without_saving_changes: 'Close without saving changes?',
|
||||
sign_yourself: 'Sign Yourself',
|
||||
set_signing_date: 'Set signing date',
|
||||
signing_date: 'Signing Date',
|
||||
@@ -371,6 +372,7 @@ const es = {
|
||||
down: 'Abajo',
|
||||
set_signing_date: 'Establecer fecha de firma',
|
||||
are_you_sure_: '¿Estás seguro?',
|
||||
close_without_saving_changes: '¿Cerrar sin guardar los cambios?',
|
||||
sign_yourself: 'Firma tú mismo',
|
||||
checked: 'Marcado',
|
||||
current_date: 'Fecha actual',
|
||||
@@ -652,6 +654,7 @@ const it = {
|
||||
remove_condition: 'Rimuovi condizione',
|
||||
add_condition: 'Aggiungi condizione',
|
||||
are_you_sure_: 'Sei sicuro?',
|
||||
close_without_saving_changes: 'Chiudere senza salvare le modifiche?',
|
||||
sign_yourself: 'Firma te stesso',
|
||||
set_signing_date: 'Imposta data di firma',
|
||||
signing_date: 'Data di firma',
|
||||
@@ -953,6 +956,7 @@ const pt = {
|
||||
down: 'Para baixo',
|
||||
set_signing_date: 'Definir data de assinatura',
|
||||
are_you_sure_: 'Tem certeza?',
|
||||
close_without_saving_changes: 'Fechar sem salvar as alterações?',
|
||||
sign_yourself: 'Assine você mesmo',
|
||||
checked: 'Marcado',
|
||||
current_date: 'Data atual',
|
||||
@@ -1234,6 +1238,7 @@ const fr = {
|
||||
remove_condition: 'Supprimer la condition',
|
||||
add_condition: 'Ajouter une condition',
|
||||
are_you_sure_: 'Êtes-vous sûr ?',
|
||||
close_without_saving_changes: 'Fermer sans enregistrer les modifications ?',
|
||||
sign_yourself: 'Signer vous-même',
|
||||
set_signing_date: 'Définir la date de signature',
|
||||
signing_date: 'Date de signature',
|
||||
@@ -1525,6 +1530,7 @@ const de = {
|
||||
remove_condition: 'Bedingung entfernen',
|
||||
add_condition: 'Bedingung hinzufügen',
|
||||
are_you_sure_: 'Sind Sie sicher?',
|
||||
close_without_saving_changes: 'Ohne Speichern der Änderungen schließen?',
|
||||
sign_yourself: 'Selbst unterschreiben',
|
||||
set_signing_date: 'Unterzeichnungsdatum festlegen',
|
||||
signing_date: 'Unterzeichnungsdatum',
|
||||
@@ -1816,6 +1822,7 @@ const nl = {
|
||||
remove_condition: 'Voorwaarde verwijderen',
|
||||
add_condition: 'Voorwaarde toevoegen',
|
||||
are_you_sure_: 'Weet u het zeker?',
|
||||
close_without_saving_changes: 'Sluiten zonder wijzigingen op te slaan?',
|
||||
sign_yourself: 'Zelf ondertekenen',
|
||||
set_signing_date: 'Ondertekeningsdatum instellen',
|
||||
signing_date: 'Ondertekeningsdatum',
|
||||
|
||||
@@ -30,14 +30,16 @@ class SendTestWebhookRequestJob
|
||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
||||
end
|
||||
|
||||
Faraday.post(webhook_url.url,
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: Time.current.iso8601,
|
||||
data: Submitters::SerializeForWebhook.call(submitter)
|
||||
}.to_json,
|
||||
body_json = {
|
||||
event_type: 'form.completed',
|
||||
timestamp: Time.current.iso8601,
|
||||
data: Submitters::SerializeForWebhook.call(submitter)
|
||||
}.to_json
|
||||
|
||||
Faraday.post(webhook_url.url, body_json,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => USER_AGENT,
|
||||
'X-Docuseal-Signature' => WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: body_json),
|
||||
**webhook_url.secret.to_h)
|
||||
end
|
||||
end
|
||||
@@ -61,6 +61,7 @@ class AccountConfig < ApplicationRecord
|
||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
||||
TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats'
|
||||
TEMPLATE_BUILDER_AUTOSAVE_KEY = 'template_builder_autosave'
|
||||
POLICY_LINKS_KEY = 'policy_links'
|
||||
ENABLE_MCP_KEY = 'enable_mcp'
|
||||
|
||||
|
||||
@@ -25,15 +25,17 @@
|
||||
# account_id :bigint not null
|
||||
# external_id :string
|
||||
# submission_id :bigint not null
|
||||
# template_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_submitters_on_account_id_and_completed_at (account_id,completed_at) WHERE (completed_at IS NOT NULL)
|
||||
# index_submitters_on_account_id_and_id (account_id,id)
|
||||
# index_submitters_on_email (email)
|
||||
# index_submitters_on_email_and_template_id (email,template_id) WHERE (email IS NOT NULL)
|
||||
# index_submitters_on_external_id (external_id)
|
||||
# index_submitters_on_slug (slug) UNIQUE
|
||||
# index_submitters_on_submission_id (submission_id)
|
||||
# index_submitters_on_template_id_and_id (template_id,id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
@@ -68,6 +70,8 @@ class Submitter < ApplicationRecord
|
||||
|
||||
scope :completed, -> { where.not(completed_at: nil) }
|
||||
|
||||
before_validation :set_template_id, on: :create
|
||||
|
||||
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
|
||||
|
||||
def status
|
||||
@@ -124,6 +128,10 @@ class Submitter < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def set_template_id
|
||||
self.template_id = submission&.template_id
|
||||
end
|
||||
|
||||
def anonymize_email_events
|
||||
email_events.each do |event|
|
||||
event.update!(email: Digest::MD5.base64digest(event.email))
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
<% end %>
|
||||
<turbo-frame id="drawer"></turbo-frame>
|
||||
<%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %>
|
||||
<% if flash.present? %><%= render 'shared/flash' %><% end %>
|
||||
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
|
||||
<main class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
|
||||
<%= yield %>
|
||||
</div>
|
||||
</main>
|
||||
<% if flash.present? %><%= render 'shared/flash' %><% end %>
|
||||
<%= render 'shared/body_scripts' %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,6 +13,8 @@
|
||||
<%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %>
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
<main>
|
||||
<%= yield %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,8 +13,10 @@
|
||||
<%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %>
|
||||
</head>
|
||||
<body>
|
||||
<% if flash.present? %><%= render 'shared/flash' %><% end %>
|
||||
<%= yield %>
|
||||
<main>
|
||||
<%= yield %>
|
||||
</main>
|
||||
<turbo-frame id="modal"></turbo-frame>
|
||||
<% if flash.present? %><%= render 'shared/flash' %><% end %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= render 'shared/navbar_warning' %>
|
||||
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
|
||||
<nav class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
||||
<%= render 'shared/title' %>
|
||||
@@ -106,4 +106,4 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,4 +1,4 @@
|
||||
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
@@ -32,5 +32,5 @@
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||
@@ -2,7 +2,7 @@
|
||||
<% I18n.with_locale(@template.account.locale) do %>
|
||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||
<% end %>
|
||||
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
@@ -55,4 +55,4 @@
|
||||
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<% I18n.with_locale(@template.account.locale) do %>
|
||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||
<% end %>
|
||||
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
@@ -28,4 +28,4 @@
|
||||
<%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<% I18n.with_locale(@template.account.locale) do %>
|
||||
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
|
||||
<% end %>
|
||||
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
||||
<div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
||||
<div class="text-center w-full space-y-6">
|
||||
<%= render 'start_form/banner' %>
|
||||
<p class="text-xl font-semibold text-center">
|
||||
@@ -27,5 +27,5 @@
|
||||
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
</main>
|
||||
</div>
|
||||
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||
@@ -2,7 +2,7 @@
|
||||
<% I18n.with_locale(@template.account.locale) do %>
|
||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||
<% end %>
|
||||
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="text-center w-full space-y-6">
|
||||
@@ -66,4 +66,4 @@
|
||||
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||
<%= render 'start_form/policy', account: @template.account %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@
|
||||
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
|
||||
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
||||
<% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %>
|
||||
<main style="max-width: 1600px" class="mx-auto pl-4">
|
||||
<div style="max-width: 1600px" class="mx-auto pl-4">
|
||||
<% unless native_tab == 'recipients' %>
|
||||
<div class="flex justify-between items-center pr-4 sticky top-0 md:relative z-10 bg-base-100 <%= 'py-1.5' unless turbo_native_app? %>">
|
||||
<% unless turbo_native_app? %>
|
||||
@@ -394,7 +394,7 @@
|
||||
</label>
|
||||
</toggle-visible>
|
||||
<% end %>
|
||||
</main>
|
||||
</div>
|
||||
<% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %>
|
||||
<%= render 'scripts/autosize_field' %>
|
||||
<% end %>
|
||||
@@ -1,5 +1,5 @@
|
||||
<% completed_at = @submitter.completed_at || @submitter.submission.completed_at %>
|
||||
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
@@ -51,5 +51,5 @@
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||
@@ -3,7 +3,7 @@
|
||||
<% I18n.with_locale(@submitter.account.locale) do %>
|
||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
|
||||
<% end %>
|
||||
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
@@ -70,4 +70,4 @@
|
||||
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -14,7 +14,7 @@
|
||||
<% delegate_modal_id = nil %>
|
||||
<% banner_html = turbo_native_app? ? '' : local_assigns[:banner_html] %>
|
||||
<div style="max-height: -webkit-fill-available;">
|
||||
<main id="scrollbox">
|
||||
<div id="scrollbox">
|
||||
<div id="sr_only_content"></div>
|
||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
|
||||
@@ -172,7 +172,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed bottom-0 w-full h-0 z-50">
|
||||
<div class="mx-auto" style="max-width: 1000px">
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY]).to_a %>
|
||||
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY, AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY]).to_a %>
|
||||
<% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %>
|
||||
<% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %>
|
||||
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" data-accept-file-types="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
|
||||
<% autosave = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY }&.value != false %>
|
||||
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" data-accept-file-types="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-autosave="<%= autosave %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
|
||||
@@ -28,7 +28,7 @@
|
||||
</submit-form>
|
||||
</form>
|
||||
<% end %>
|
||||
<a href="<%= edit_template_path(@template) %>" class="base-button" data-turbo="false" style="flex-shrink: 0; padding: 0px 24px;">
|
||||
<a href="<%= edit_template_path(@template) %>" class="base-button" data-turbo-prefetch="false" style="flex-shrink: 0; padding: 0px 24px;">
|
||||
<%= t('exit_preview') %>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"note": "Permitted parameters are necessary for creating submitters via API"
|
||||
},
|
||||
{
|
||||
"fingerprint": "645f2f352a813105b8b2eeadc68101d2e7ea04b8e839aa1336616be56f1108ca",
|
||||
"fingerprint": "58619647d807d5bedb9499d6b1432f7f9cee385167aa97b0cb5e1b31a3db3f40",
|
||||
"note": "Safe SQL"
|
||||
},
|
||||
{
|
||||
"fingerprint": "5bcf4e6426d17b0efd98e47fde7c012f09500b9595af9cff226cf09341c023be",
|
||||
"fingerprint": "8538c7cca701df19fdc3c1c6c5b7b0a1a3acacc19c840d63147553014d60ffa0",
|
||||
"note": "Safe SQL"
|
||||
},
|
||||
{
|
||||
|
||||
+1
-9
@@ -25,8 +25,6 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :api, defaults: { format: :json } do
|
||||
resource :user, only: %i[show]
|
||||
resources :attachments, only: %i[create]
|
||||
resources :submitter_email_clicks, only: %i[create]
|
||||
resources :submitter_form_views, only: %i[create]
|
||||
resources :submitters, only: %i[index show update]
|
||||
resources :submissions, only: %i[index show create update destroy] do
|
||||
@@ -128,13 +126,7 @@ Rails.application.routes.draw do
|
||||
controller: 'api/active_storage_blobs_proxy'
|
||||
|
||||
if Docuseal.multitenant?
|
||||
resource :blobs_proxy_legacy, only: %i[show],
|
||||
path: '/blobs/proxy/:signed_id/*filename',
|
||||
controller: 'api/active_storage_blobs_proxy_legacy',
|
||||
as: :rails_blob
|
||||
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service
|
||||
put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service
|
||||
post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
|
||||
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service if Rails.env.local?
|
||||
|
||||
ActiveSupport.run_load_hooks(:multitenant_routes, self)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddTemplateIdToSubmitters < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :submitters, :template_id, :bigint, if_not_exists: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PopulateSubmitterTemplateId < ActiveRecord::Migration[8.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
class MigrationSubmitter < ApplicationRecord
|
||||
self.table_name = 'submitters'
|
||||
end
|
||||
|
||||
class MigrationSubmission < ApplicationRecord
|
||||
self.table_name = 'submissions'
|
||||
end
|
||||
|
||||
def up
|
||||
return if ENV['MULTITENANT'] == 'true'
|
||||
|
||||
max_id = MigrationSubmitter.maximum(:id)
|
||||
|
||||
return unless max_id
|
||||
|
||||
submission_template_id =
|
||||
Arel::Nodes::Grouping.new(
|
||||
MigrationSubmission.arel_table.project(MigrationSubmission.arel_table[:template_id])
|
||||
.where(MigrationSubmission.arel_table[:id].eq(MigrationSubmitter.arel_table[:submission_id]))
|
||||
.ast
|
||||
)
|
||||
|
||||
(1..max_id).step(10_000) do |start_id|
|
||||
MigrationSubmitter.where(template_id: nil, id: start_id...(start_id + 10_000))
|
||||
.update_all(template_id: submission_template_id)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddSubmittersTemplateIdIndexes < ActiveRecord::Migration[8.1]
|
||||
def up
|
||||
add_index :submitters, %i[email template_id], where: 'email IS NOT NULL', if_not_exists: true
|
||||
add_index :submitters, %i[template_id id], if_not_exists: true
|
||||
remove_index :submitters, :email, if_exists: true
|
||||
end
|
||||
|
||||
def down
|
||||
add_index :submitters, :email, if_not_exists: true
|
||||
remove_index :submitters, %i[template_id id], if_exists: true
|
||||
remove_index :submitters, %i[email template_id], if_exists: true
|
||||
end
|
||||
end
|
||||
+4
-2
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_10_25_120002) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "btree_gin"
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
@@ -424,6 +424,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
|
||||
t.datetime "sent_at"
|
||||
t.string "slug", null: false
|
||||
t.bigint "submission_id", null: false
|
||||
t.bigint "template_id"
|
||||
t.string "timezone"
|
||||
t.string "ua"
|
||||
t.datetime "updated_at", null: false
|
||||
@@ -431,10 +432,11 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
|
||||
t.text "values", null: false
|
||||
t.index ["account_id", "completed_at"], name: "index_submitters_on_account_id_and_completed_at", where: "(completed_at IS NOT NULL)"
|
||||
t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id"
|
||||
t.index ["email"], name: "index_submitters_on_email"
|
||||
t.index ["email", "template_id"], name: "index_submitters_on_email_and_template_id", where: "(email IS NOT NULL)"
|
||||
t.index ["external_id"], name: "index_submitters_on_external_id"
|
||||
t.index ["slug"], name: "index_submitters_on_slug", unique: true
|
||||
t.index ["submission_id"], name: "index_submitters_on_submission_id"
|
||||
t.index ["template_id", "id"], name: "index_submitters_on_template_id_and_id"
|
||||
end
|
||||
|
||||
create_table "template_accesses", force: :cascade do |t|
|
||||
|
||||
@@ -7,10 +7,7 @@ class ApiPathConsiderJsonMiddleware
|
||||
|
||||
def call(env)
|
||||
if env['PATH_INFO'].starts_with?('/api') &&
|
||||
(!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') &&
|
||||
!env['PATH_INFO'].ends_with?('/attachments') &&
|
||||
!env['PATH_INFO'].ends_with?('/submitter_sms_clicks') &&
|
||||
!env['PATH_INFO'].ends_with?('/submitter_email_clicks')
|
||||
!env['PATH_INFO'].ends_with?('/submitter_sms_clicks')
|
||||
env['CONTENT_TYPE'] = 'application/json'
|
||||
end
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ module ReplaceEmailVariables
|
||||
DOCUMENTS_LINKS = /\{+documents\.links\}+/i
|
||||
DOCUMENTS_LINK = /\{+documents\.link\}+/i
|
||||
|
||||
ESCAPED_BRACES = "\u{E000}\u{E001}"
|
||||
|
||||
EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil)
|
||||
|
||||
module_function
|
||||
@@ -86,7 +88,11 @@ module ReplaceEmailVariables
|
||||
build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip)
|
||||
end
|
||||
|
||||
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
|
||||
text = replace(text, SENDER_EMAIL, html_escape:) do
|
||||
submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@')
|
||||
end
|
||||
|
||||
text.tr(ESCAPED_BRACES, '{}')
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
@@ -124,6 +130,8 @@ module ReplaceEmailVariables
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
|
||||
end
|
||||
elsif (mask = field.dig('preferences', 'mask').presence)
|
||||
TextUtils.mask_value(value[field['uuid']].to_s, mask)
|
||||
else
|
||||
value[field&.dig('uuid')]
|
||||
end
|
||||
@@ -134,10 +142,12 @@ module ReplaceEmailVariables
|
||||
|
||||
def replace(text, var, html_escape: false)
|
||||
text.gsub(var) do
|
||||
value = yield(Regexp.last_match).to_s.tr('{}', ESCAPED_BRACES)
|
||||
|
||||
if html_escape
|
||||
ERB::Util.html_escape(yield(Regexp.last_match))
|
||||
ERB::Util.html_escape(value)
|
||||
else
|
||||
yield(Regexp.last_match)
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+20
-7
@@ -3,6 +3,8 @@
|
||||
module SearchEntries
|
||||
MAX_VALUE_LENGTH = 100
|
||||
|
||||
MUTEX = Mutex.new
|
||||
|
||||
UUID_REGEXP = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
|
||||
|
||||
module_function
|
||||
@@ -70,10 +72,10 @@ module SearchEntries
|
||||
|
||||
arel
|
||||
else
|
||||
"tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*')::tsquery"
|
||||
"tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*')::tsquery"
|
||||
end
|
||||
|
||||
[sql, { keyword: }]
|
||||
[sql, { keyword:, dictionary: default_dictionary }]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,7 +85,7 @@ module SearchEntries
|
||||
Arel.sql("ngram @@ (quote_literal(:term#{terms.size - 1}) || ':' || :weight)::tsquery")
|
||||
else
|
||||
Arel.sql(<<~SQL.squish)
|
||||
(quote_literal(coalesce((ts_lexize('english_stem', :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery
|
||||
(quote_literal(coalesce((ts_lexize(:dictionary, :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery
|
||||
SQL
|
||||
end
|
||||
|
||||
@@ -91,7 +93,7 @@ module SearchEntries
|
||||
index = terms.index(term)
|
||||
|
||||
arel = Arel.sql(<<~SQL.squish)
|
||||
(quote_literal(coalesce((ts_lexize('english_stem', :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery
|
||||
(quote_literal(coalesce((ts_lexize(:dictionary, :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery
|
||||
SQL
|
||||
|
||||
acc ? Arel::Nodes::InfixOperation.new('&&', arel, acc) : arel
|
||||
@@ -109,7 +111,8 @@ module SearchEntries
|
||||
)
|
||||
end
|
||||
|
||||
[query.to_sql, terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:)]
|
||||
[query.to_sql,
|
||||
terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:, dictionary: default_dictionary)]
|
||||
end
|
||||
|
||||
def build_weights_wildcard_tsquery(keyword, weight)
|
||||
@@ -122,11 +125,11 @@ module SearchEntries
|
||||
SQL
|
||||
else
|
||||
<<~SQL.squish
|
||||
tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*' || :weight)::tsquery
|
||||
tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*' || :weight)::tsquery
|
||||
SQL
|
||||
end
|
||||
|
||||
[sql, { keyword:, weight: }]
|
||||
[sql, { keyword:, weight:, dictionary: default_dictionary }]
|
||||
end
|
||||
|
||||
def index_submitter(submitter)
|
||||
@@ -256,4 +259,14 @@ module SearchEntries
|
||||
|
||||
ngrams.uniq { |e| e.sub(/':[\d,]/, "':1") }.join(' ')
|
||||
end
|
||||
|
||||
def default_dictionary
|
||||
@default_dictionary || MUTEX.synchronize do
|
||||
@default_dictionary ||= SearchEntry.connection.select_value(<<~SQL.squish)
|
||||
SELECT mapdict::regdictionary::text FROM pg_ts_config_map
|
||||
WHERE mapcfg = get_current_ts_config() AND maptokentype = 1
|
||||
ORDER BY mapseqno LIMIT 1
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -884,6 +884,8 @@ module Submissions
|
||||
end
|
||||
|
||||
def maybe_flatten_pdf(pdf)
|
||||
pdf.catalog.delete(:AcroForm) if pdf.catalog[:AcroForm].is_a?(Integer)
|
||||
|
||||
pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances]
|
||||
pdf.acro_form&.flatten
|
||||
rescue HexaPDF::MissingGlyphError
|
||||
|
||||
@@ -65,6 +65,7 @@ module Submitters
|
||||
submissions = submissions.where(source:) if source
|
||||
|
||||
Submitter
|
||||
.where(template_id: template.id)
|
||||
.where(submission: submissions)
|
||||
.order(id: :desc)
|
||||
.where(declined_at: nil)
|
||||
|
||||
@@ -9,6 +9,8 @@ module Submitters
|
||||
PHONE_REGEXP = /[+\d()\s-]+/
|
||||
NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze
|
||||
REQUIRED_FIELD_TYPES = %w[payment kba verification].freeze
|
||||
CHOICE_FIELD_TYPES = %w[radio multiple select].freeze
|
||||
BOOLEAN_VALUES = [true, false].freeze
|
||||
|
||||
STRFTIME_MAP = {
|
||||
'hour' => '%-k',
|
||||
@@ -55,6 +57,7 @@ module Submitters
|
||||
validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request)
|
||||
|
||||
if params[:completed] == 'true'
|
||||
validate_invite_submitters!(submitter)
|
||||
maybe_invite_via_field(submitter, request)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request)
|
||||
@@ -504,7 +507,7 @@ module Submitters
|
||||
submitter
|
||||
end
|
||||
|
||||
def validate_value!(_value, field, _params, submitter, _request)
|
||||
def validate_value!(value, field, _params, submitter, _request)
|
||||
raise ValidationError, 'Missing field' unless field
|
||||
raise ValidationError, 'Invalid field' if field['submitter_uuid'] != submitter.uuid
|
||||
|
||||
@@ -514,7 +517,34 @@ module Submitters
|
||||
raise ValidationError, 'Read-only field'
|
||||
end
|
||||
|
||||
raise ValidationError, 'Value is invalid' if field['type'] == 'checkbox' && !value.in?(BOOLEAN_VALUES)
|
||||
|
||||
validate_choice_value!(value, field, submitter) if field['type'].in?(CHOICE_FIELD_TYPES)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def validate_choice_value!(value, field, submitter)
|
||||
return if value.blank?
|
||||
|
||||
raise ValidationError, 'Value is invalid' unless value.is_a?(field['type'] == 'multiple' ? Array : String)
|
||||
|
||||
option_names = field['options'].to_a.map.with_index do |option, index|
|
||||
option['value'].presence ||
|
||||
"#{I18n.t('option', locale: submitter.metadata.fetch('lang', submitter.account.locale))} #{index + 1}"
|
||||
end
|
||||
|
||||
raise ValidationError, 'Value is invalid' unless Array.wrap(value).all? { |item| option_names.include?(item) }
|
||||
end
|
||||
|
||||
def validate_invite_submitters!(submitter)
|
||||
submission = submitter.submission
|
||||
|
||||
is_pending_invite = (submission.template_submitters || submission.template.submitters).any? do |s|
|
||||
s['invite_by_uuid'] == submitter.uuid && submission.submitters.none? { |e| e.uuid == s['uuid'] }
|
||||
end
|
||||
|
||||
raise ValidationError, 'Invite required' if is_pending_invite
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ module Templates
|
||||
ZIP_CONTENT_TYPE = 'application/zip'
|
||||
X_ZIP_CONTENT_TYPE = 'application/x-zip-compressed'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
WORD_EXTENSIONS = %w[.docx .doc .rtf].freeze
|
||||
WORD_EXTENSIONS = %w[.docx .doc .odt .rtf].freeze
|
||||
DOCUMENT_EXTENSIONS = %w[.docx .doc .xlsx .xls .odt .rtf].freeze
|
||||
|
||||
DOCUMENT_CONTENT_TYPES = %w[
|
||||
|
||||
@@ -180,8 +180,11 @@ module VerifyPdfSignature
|
||||
|
||||
return false if byte_range.size != 4 || byte_range.any?(&:negative?) || byte_range[0].positive?
|
||||
return false if signature.signed_end > io.size
|
||||
return false if byte_range[2] != byte_range[1] + (signature.contents.bytesize * 2) + 2
|
||||
|
||||
byte_range[2] == byte_range[1] + (signature.contents.bytesize * 2) + 2
|
||||
io.seek(byte_range[1])
|
||||
|
||||
io.read(byte_range[2] - byte_range[1]).casecmp?("<#{signature.contents.unpack1('H*')}>")
|
||||
end
|
||||
|
||||
def unsigned_changes?(document, io, signed_end)
|
||||
|
||||
Reference in new issue
Block a user