Compare commits

..

No commits in common. '744d45d2c588d3d610c284190cb6a8a5f158ee0b' and 'daaa289a5c6830198d7b9d8dec39bad9c03b10ad' have entirely different histories.

@ -33,16 +33,10 @@ module Api
else else
http_cache_forever public: true do http_cache_forever public: true do
response.headers['Accept-Ranges'] = 'bytes' response.headers['Accept-Ranges'] = 'bytes'
response.headers['Content-Length'] = blob.byte_size.to_s
if request.head?
response.headers['Content-Type'] = blob.content_type_for_serving
head :ok
else
send_blob_stream blob, disposition: params[:disposition] send_blob_stream blob, disposition: params[:disposition]
end end
response.headers['Content-Length'] = blob.byte_size.to_s
end
end end
end end
@ -63,6 +57,8 @@ module Api
return if !require_ttl && !require_auth return if !require_ttl && !require_auth
end end
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
raise CanCan::AccessDenied raise CanCan::AccessDenied
end end
end end

@ -203,15 +203,10 @@ module Api
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to') submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('require_phone_2fa') if submitter_preferences.key?('require_phone_2fa')
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa'] submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
end end
if submitter_preferences.key?('require_email_2fa')
submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_2fa']
end
if submitter_preferences.key?('go_to_last') if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last'] submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end end

@ -41,7 +41,7 @@ class PreviewDocumentPageController < ActionController::API
end end
def find_or_create_document_tempfile_path(attachment) def find_or_create_document_tempfile_path(attachment)
file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}" file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f| File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
f.flock(File::LOCK_EX) f.flock(File::LOCK_EX)

@ -1,14 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class RevealAccessTokenController < ApplicationController class RevealAccessTokenController < ApplicationController
rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda {
Rollbar.error('Rate limit api key') if defined?(Rollbar)
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
locals: { error_message: I18n.t(:too_many_attempts) }),
status: :unprocessable_content
}
def show def show
authorize!(:manage, current_user.access_token) authorize!(:manage, current_user.access_token)
end end

@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController
template = Template.find_by!(slug: params[:template_slug]) template = Template.find_by!(slug: params[:template_slug])
@submitter = @submitter =
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase) Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
elsif params[:submission_slug] elsif params[:submission_slug]
submission = Submission.find_by(slug: params[:submission_slug]) submission = Submission.find_by(slug: params[:submission_slug])
@ -27,11 +27,9 @@ class SendSubmissionEmailController < ApplicationController
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug]) @submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
end end
if @submitter
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter) SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
end
respond_to do |f| respond_to do |f|
f.html { render :success } f.html { render :success }

@ -1,10 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class TemplatesController < ApplicationController class TemplatesController < ApplicationController
TEMPLATE_FIELDS = %i[id author_id folder_id external_id name slug
schema fields submitters variables_schema preferences
shared_link source archived_at created_at updated_at].freeze
load_and_authorize_resource :template load_and_authorize_resource :template
def show def show
@ -37,11 +33,10 @@ class TemplatesController < ApplicationController
).call ).call
@template_data = @template_data =
@template.as_json(only: TEMPLATE_FIELDS).merge( @template.as_json.merge(
documents: @template.schema_documents.as_json( documents: @template.schema_documents.as_json(
only: %i[id uuid],
methods: %i[metadata signed_key], methods: %i[metadata signed_key],
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } } include: { preview_images: { methods: %i[url metadata filename] } }
) )
).to_json ).to_json

@ -3,7 +3,7 @@
class TestingAccountsController < ApplicationController class TestingAccountsController < ApplicationController
skip_authorization_check only: :destroy skip_authorization_check only: :destroy
def create def show
authorize!(:manage, current_account) authorize!(:manage, current_account)
authorize!(:manage, current_user) authorize!(:manage, current_user)

@ -11,7 +11,6 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!this.canvas) return if (!this.canvas) return
if (!this.canvas.parentNode?.clientWidth) return
const { width, height } = this.canvas const { width, height } = this.canvas
@ -90,7 +89,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
} }
redrawCanvas (oldWidth, oldHeight) { redrawCanvas (oldWidth, oldHeight) {
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) { if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
const sx = this.canvas.width / oldWidth const sx = this.canvas.width / oldWidth
const sy = this.canvas.height / oldHeight const sy = this.canvas.height / oldHeight

@ -14,7 +14,6 @@ export default targetable(class extends HTMLElement {
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!this.canvas) return if (!this.canvas) return
if (!this.canvas.parentNode?.clientWidth) return
const { width, height } = this.canvas const { width, height } = this.canvas
@ -81,7 +80,7 @@ export default targetable(class extends HTMLElement {
} }
redrawCanvas (oldWidth, oldHeight) { redrawCanvas (oldWidth, oldHeight) {
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) { if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
const sx = this.canvas.width / oldWidth const sx = this.canvas.width / oldWidth
const sy = this.canvas.height / oldHeight const sy = this.canvas.height / oldHeight

@ -42,25 +42,10 @@ export default {
const areas = {} const areas = {}
this.field.areas?.forEach((area) => { this.field.areas?.forEach((area) => {
areas[area.attachment_uuid] ||= [] areas[area.attachment_uuid + area.page] ||= area
areas[area.attachment_uuid].push(area)
}) })
const sortedAreas = Object.values(areas).reduce((acc, group) => { return Object.values(areas).slice(0, 6)
const seen = {}
const sortedGroup = [...group].sort((a, b) => a.page - b.page)
sortedGroup.forEach((area) => {
if (!seen[area.page]) {
seen[area.page] = true
acc.push(area)
}
})
return acc
}, [])
return sortedAreas.slice(0, 6)
} }
} }
} }

@ -544,7 +544,6 @@ export default {
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!this.$refs.canvas) return if (!this.$refs.canvas) return
if (!this.$refs.canvas.parentNode?.clientWidth) return
const { width, height } = this.$refs.canvas const { width, height } = this.$refs.canvas
@ -587,7 +586,7 @@ export default {
redrawCanvas (oldWidth, oldHeight) { redrawCanvas (oldWidth, oldHeight) {
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && canvas.width > 0 && canvas.height > 0) { if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
const sx = canvas.width / oldWidth const sx = canvas.width / oldWidth
const sy = canvas.height / oldHeight const sy = canvas.height / oldHeight

@ -1012,13 +1012,6 @@ export default {
fieldsDragFieldRef: () => ref(), fieldsDragFieldRef: () => ref(),
customDragFieldRef: () => ref(), customDragFieldRef: () => ref(),
selectedAreasRef: () => ref([]), selectedAreasRef: () => ref([]),
attachmentUuidsIndex () {
return this.template.schema.reduce((acc, e, index) => {
acc[e.attachment_uuid] = index
return acc
}, {})
},
language () { language () {
return this.locale.split('-')[0].toLowerCase() return this.locale.split('-')[0].toLowerCase()
}, },
@ -1670,9 +1663,20 @@ export default {
this.save() this.save()
} }
}, },
compareAreas (a, b) { findFieldInsertIndex (field) {
const aAttIdx = this.attachmentUuidsIndex[a.attachment_uuid] if (!field.areas?.length) return -1
const bAttIdx = this.attachmentUuidsIndex[b.attachment_uuid]
const area = field.areas[0]
const attachmentUuidsIndex = this.template.schema.reduce((acc, e, index) => {
acc[e.attachment_uuid] = index
return acc
}, {})
const compareAreas = (a, b) => {
const aAttIdx = attachmentUuidsIndex[a.attachment_uuid]
const bAttIdx = attachmentUuidsIndex[b.attachment_uuid]
if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx
if (a.page !== b.page) return a.page - b.page if (a.page !== b.page) return a.page - b.page
@ -1684,11 +1688,7 @@ export default {
if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x
return aY - bY return aY - bY
}, }
findFieldInsertIndex (field) {
if (!field.areas?.length) return -1
const area = field.areas[0]
let closestBeforeIndex = -1 let closestBeforeIndex = -1
let closestBeforeArea = null let closestBeforeArea = null
@ -1698,15 +1698,15 @@ export default {
this.template.fields.forEach((f, index) => { this.template.fields.forEach((f, index) => {
if (f.submitter_uuid === field.submitter_uuid) { if (f.submitter_uuid === field.submitter_uuid) {
(f.areas || []).forEach((a) => { (f.areas || []).forEach((a) => {
const cmp = this.compareAreas(a, area) const cmp = compareAreas(a, area)
if (cmp < 0) { if (cmp < 0) {
if (!closestBeforeArea || (this.compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) { if (!closestBeforeArea || (compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) {
closestBeforeIndex = index closestBeforeIndex = index
closestBeforeArea = a closestBeforeArea = a
} }
} else { } else {
if (!closestAfterArea || (this.compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) { if (!closestAfterArea || (compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) {
closestAfterIndex = index closestAfterIndex = index
closestAfterArea = a closestAfterArea = a
} }
@ -1729,17 +1729,6 @@ export default {
this.template.fields.push(field) this.template.fields.push(field)
} }
}, },
insertArea (field, area) {
field.areas ||= []
const insertIndex = field.areas.findIndex((a) => this.compareAreas(a, area) > 0)
if (insertIndex === -1) {
field.areas.push(area)
} else {
field.areas.splice(insertIndex, 0, area)
}
},
insertDetectedField (field) { insertDetectedField (field) {
if (!this.withDetectExistingFields || !field.name) { if (!this.withDetectExistingFields || !field.name) {
this.insertField(field) this.insertField(field)
@ -1755,7 +1744,7 @@ export default {
if (existingField) { if (existingField) {
existingField.areas = existingField.areas || [] existingField.areas = existingField.areas || []
field.areas.forEach((area) => this.insertArea(existingField, area)) existingField.areas.push(...(field.areas || []))
} else { } else {
const customField = this.detectCustomFieldsIndex[indexKey] || this.detectCustomFieldsIndex[nameKey] const customField = this.detectCustomFieldsIndex[indexKey] || this.detectCustomFieldsIndex[nameKey]
@ -2260,7 +2249,7 @@ export default {
fieldUuidIndex[field.uuid] = newField fieldUuidIndex[field.uuid] = newField
this.insertArea(newField, newArea) newField.areas.push(newArea)
newAreas.push(newArea) newAreas.push(newArea)
if (['radio', 'multiple'].includes(field.type) && field.options?.length) { if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
@ -2373,7 +2362,17 @@ export default {
area.y -= area.h / 2 area.y -= area.h / 2
} }
this.insertArea(this.drawField, area) this.drawField.areas ||= []
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
})
if (insertBeforeAreaIndex !== -1) {
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
} else {
this.drawField.areas.push(area)
}
if (this.template.fields.indexOf(this.drawField) === -1) { if (this.template.fields.indexOf(this.drawField) === -1) {
this.insertField(this.drawField) this.insertField(this.drawField)
@ -2514,7 +2513,9 @@ export default {
delete field.height delete field.height
} }
this.insertArea(field, fieldArea) field.areas ||= []
field.areas.push(fieldArea)
if (this.selectedAreasRef.value.length < 2) { if (this.selectedAreasRef.value.length < 2) {
this.selectedAreasRef.value = [fieldArea] this.selectedAreasRef.value = [fieldArea]
@ -2584,7 +2585,7 @@ export default {
} }
} }
this.insertArea(field, fieldArea) field.areas.push(fieldArea)
}) })
} else { } else {
const fieldArea = { const fieldArea = {

@ -786,27 +786,17 @@ export default {
}, },
copyToAllPages (field) { copyToAllPages (field) {
const areaString = JSON.stringify(field.areas[0]) const areaString = JSON.stringify(field.areas[0])
const newAreas = []
const existingAreasIndex = field.areas.reduce((acc, area) => {
acc[`${area.attachment_uuid}-${area.page}`] = area
return acc
}, {})
this.template.schema.forEach((item) => {
const attachment = this.template.documents.find((d) => d.uuid === item.attachment_uuid)
this.template.documents.forEach((attachment) => {
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
for (let page = 0; page <= numberOfPages - 1; page++) { for (let page = 0; page <= numberOfPages - 1; page++) {
const existing = existingAreasIndex[`${attachment.uuid}-${page}`] if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
newAreas.push(existing || { ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page }) }
} }
}) })
field.areas = newAreas
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1]) this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
this.$emit('save') this.$emit('save')

@ -143,8 +143,8 @@
> >
<input <input
type="hidden" type="hidden"
name="oauth_data" name="state"
:value="oauthData" :value="oauthState"
autocomplete="off" autocomplete="off"
> >
<input <input
@ -334,7 +334,7 @@ export default {
authenticityToken () { authenticityToken () {
return document.querySelector('meta[name="csrf-token"]')?.content return document.querySelector('meta[name="csrf-token"]')?.content
}, },
oauthData () { oauthState () {
const params = new URLSearchParams('') const params = new URLSearchParams('')
params.set('redir', document.location.href) params.set('redir', document.location.href)

@ -228,7 +228,7 @@ export default {
'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/drive.file' 'https://www.googleapis.com/auth/drive.file'
].join(' '), ].join(' '),
oauth_data: new URLSearchParams({ state: new URLSearchParams({
redir: `/templates/${this.templateId}/edit?google_drive_open=1` redir: `/templates/${this.templateId}/edit?google_drive_open=1`
}).toString() }).toString()
} }

@ -101,18 +101,13 @@ class Submission < ApplicationRecord
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists) .and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
} }
scope :expired, lambda { scope :expired, -> { pending.where(expire_at: ..Time.current) }
where(expire_at: ..Time.current)
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
enum :source, { enum :source, {
invite: 'invite', invite: 'invite',
bulk: 'bulk', bulk: 'bulk',
api: 'api', api: 'api',
embed: 'embed', embed: 'embed',
mcp: 'mcp',
link: 'link' link: 'link'
}, scope: false, prefix: true }, scope: false, prefix: true

@ -18,7 +18,24 @@
<%= f.button button_title(title: t(:sign_in), disabled_with: t(:signing_in)), class: 'base-button' %> <%= f.button button_title(title: t(:sign_in), disabled_with: t(:signing_in)), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<%= render 'omniauthable' %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4">
<% if User.omniauth_providers.include?(:google_oauth2) %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: t('sign_in_with_google'), icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %>
<% if User.omniauth_providers.include?(:microsoft_office365) %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: t('sign_in_with_microsoft'), icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %>
</div>
<% end %>
<%= render 'extra_links' %> <%= render 'extra_links' %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
</div> </div>

@ -20,10 +20,9 @@
</head> </head>
<body> <body>
<% if params[:modal].present? %> <% if params[:modal].present? %>
<% modal_uri = Addressable::URI.parse(params[:modal]) %> <% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
<% url_params = Rails.application.routes.recognize_path(modal_uri.path, method: :get) %>
<% if url_params[:action] == 'new' %> <% if url_params[:action] == 'new' %>
<open-modal src="<%= url_for(**url_params, params: modal_uri.query_values) %>"></open-modal> <open-modal src="<%= url_for(url_params) %>"></open-modal>
<% end %> <% end %>
<% end %> <% end %>
<turbo-frame id="modal"></turbo-frame> <turbo-frame id="modal"></turbo-frame>

@ -1,6 +1,6 @@
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub"> <a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-medium"> <span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %> <%= svg_icon('start', class: 'h-3 w-3') %>
<span>12k</span> <span>11k</span>
</span> </span>
</a> </a>

@ -59,7 +59,7 @@
</li> </li>
<% end %> <% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> <% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full py-1' } do |f| %> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle"> <label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %> <%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>

@ -97,7 +97,7 @@
<% end %> <% end %>
<%= render 'shared/settings_nav_extra2' %> <%= render 'shared/settings_nav_extra2' %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> <% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full' } do |f| %>
<li> <li>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle"> <label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full"> <span class="mr-2 w-full">

@ -1,5 +1,5 @@
<% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %> <% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'flex' } do |f| %> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
<label class="flex items-center justify-between" for="testing_toggle"> <label class="flex items-center justify-between" for="testing_toggle">
<span class="mr-2 text-lg"> <span class="mr-2 text-lg">
<%= t('test_mode') %> <%= t('test_mode') %>

@ -5,9 +5,9 @@
<% font = field.dig('preferences', 'font') %> <% font = field.dig('preferences', 'font') %>
<% font_type = field.dig('preferences', 'font_type') %> <% font_type = field.dig('preferences', 'font_type') %>
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %> <% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
<field-value dir="auto" aria-hidden="true" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? && color.match?(Templates::COLOR_REGEXP) %><%= "background: #{bg_color}; " if bg_color.present? && bg_color.match?(Templates::COLOR_REGEXP) %>width: <%= area['w'].to_f * 100 %>%; height: <%= area['h'].to_f * 100 %>%; left: <%= area['x'].to_f * 100 %>%; top: <%= area['y'].to_f * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)"> <field-value dir="auto" aria-hidden="true" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
<% if field['type'] == 'signature' %> <% if field['type'] == 'signature' %>
<% is_narrow = area['h'].to_f.positive? && ((area['w'].to_f * local_assigns[:page_width]) / (area['h'].to_f * local_assigns[:page_height])) > 4.5 %> <% is_narrow = area['h'].positive? && ((area['w'] * local_assigns[:page_width]).to_f / (area['h'] * local_assigns[:page_height])) > 4.5 %>
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>"> <div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%"> <div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
@ -62,9 +62,9 @@
<% end %> <% end %>
<% end %> <% end %>
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %> <% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
<% cell_width = area['cell_w'].to_f / area['w'] * 100 %> <% cell_width = area['cell_w'] / area['w'] * 100 %>
<div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>"> <div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>">
<% (0..(area['w'].to_f / area['cell_w']).ceil).each do |index| %> <% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
<% if value[index] %> <% if value[index] %>
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div> <div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
<% end %> <% end %>
@ -83,14 +83,14 @@
</div> </div>
<% elsif field['type'] == 'strikethrough' %> <% elsif field['type'] == 'strikethrough' %>
<div class="w-full h-full flex items-center justify-center"> <div class="w-full h-full flex items-center justify-center">
<% if (((1000.0 / local_assigns[:page_width]) * local_assigns[:page_height]) * area['h'].to_f) < 40 %> <% if (((1000.0 / local_assigns[:page_width]) * local_assigns[:page_height]) * area['h']) < 40 %>
<svg width="100%" height="100%"> <svg width="100%" height="100%">
<line x1="0" y1="50%" x2="100%" y2="50%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line> <line x1="0" y1="50%" x2="100%" y2="50%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
</svg> </svg>
<% else %> <% else %>
<svg xmlns="http://www.w3.org/2000/svg" style="overflow: visible; width: calc(100% - 6px); height: calc(100% - 6px); width: calc(100% - 0.6cqmin); height: calc(100% - 0.6cqmin)"> <svg xmlns="http://www.w3.org/2000/svg" style="overflow: visible; width: calc(100% - 6px); height: calc(100% - 6px); width: calc(100% - 0.6cqmin); height: calc(100% - 0.6cqmin)">
<line x1="0" y1="0" x2="100%" y2="100%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line> <line x1="0" y1="0" x2="100%" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
<line x1="100%" y1="0" x2="0" y2="100%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line> <line x1="100%" y1="0" x2="0" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
</svg> </svg>
<% end %> <% end %>
</div> </div>

@ -131,7 +131,7 @@
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %> <% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
<% submitter_index = submitters_order_index[submitter.uuid] %> <% submitter_index = submitters_order_index[submitter.uuid] %>
<% bg_class = bg_classes[submitter_index % bg_classes.size] %> <% bg_class = bg_classes[submitter_index % bg_classes.size] %>
<div class="absolute overflow-visible" style="width: <%= area['w'].to_f * 100 %>%; height: <%= area['h'].to_f * 100 %>%; left: <%= area['x'].to_f * 100 %>%; top: <%= area['y'].to_f * 100 %>%;"> <div class="absolute overflow-visible" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%;">
<div class="flex h-full w-full bg-opacity-80 justify-center items-center <%= bg_class %>"> <div class="flex h-full w-full bg-opacity-80 justify-center items-center <%= bg_class %>">
<%= svg_icon(SubmissionsController::FIELD_ICONS.fetch(field['type'], 'text_size'), class: 'max-h-10 w-full h-full stroke-2 opacity-50') %> <%= svg_icon(SubmissionsController::FIELD_ICONS.fetch(field['type'], 'text_size'), class: 'max-h-10 w-full h-full stroke-2 opacity-50') %>
</div> </div>

@ -25,11 +25,9 @@
<div class="flex gap-2 mt-3"> <div class="flex gap-2 mt-3">
<div class="relative flex-grow"> <div class="relative flex-grow">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly> <input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly>
<% if @template.variables_schema.blank? && !@template.preferences&.dig('require_email_2fa') && !@template.preferences&.dig('require_phone_2fa') %>
<a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>"> <a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>">
<%= svg_icon('qrcode', class: 'w-6 h-6') %> <%= svg_icon('qrcode', class: 'w-6 h-6') %>
</a> </a>
<% end %>
</div> </div>
<check-on-click data-element-id="template_shared_link"> <check-on-click data-element-id="template_shared_link">
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>

@ -942,7 +942,6 @@ en: &en
embed: Embedding embed: Embedding
invite: Invite invite: Invite
link: Link link: Link
mcp: MCP
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}' send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
bounce_email_html: '<b>Email bounced</b> %{submitter_name}' bounce_email_html: '<b>Email bounced</b> %{submitter_name}'
@ -1983,7 +1982,6 @@ es: &es
embed: Integración embed: Integración
invite: Invitación invite: Invitación
link: Enlace link: Enlace
mcp: MCP
submission_event_names: submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}' send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}' bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}'
@ -3025,7 +3023,6 @@ it: &it
embed: Incorporamento embed: Incorporamento
invite: Invito invite: Invito
link: Link link: Link
mcp: MCP
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email inviata</b> a %{submitter_name}' send_email_to_html: '<b>Email inviata</b> a %{submitter_name}'
bounce_email_html: '<b>Email respinta</b> %{submitter_name}' bounce_email_html: '<b>Email respinta</b> %{submitter_name}'
@ -4063,7 +4060,6 @@ fr: &fr
embed: Embedding embed: Embedding
invite: Invitation invite: Invitation
link: Lien link: Lien
mcp: MCP
submission_event_names: submission_event_names:
send_email_to_html: "<b>Email envoyé</b> à %{submitter_name}" send_email_to_html: "<b>Email envoyé</b> à %{submitter_name}"
bounce_email_html: "<b>Email rejeté</b> %{submitter_name}" bounce_email_html: "<b>Email rejeté</b> %{submitter_name}"
@ -5104,7 +5100,6 @@ pt: &pt
embed: Incorporação embed: Incorporação
invite: Convite invite: Convite
link: Link link: Link
mcp: MCP
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email enviado</b> para %{submitter_name}' send_email_to_html: '<b>Email enviado</b> para %{submitter_name}'
bounce_email_html: '<b>Email não entregue</b> %{submitter_name}' bounce_email_html: '<b>Email não entregue</b> %{submitter_name}'
@ -6145,7 +6140,6 @@ de: &de
embed: Einbettung embed: Einbettung
invite: Einladung invite: Einladung
link: Link link: Link
mcp: MCP
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}' send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
bounce_email_html: '<b>E-Mail unzustellbar</b> %{submitter_name}' bounce_email_html: '<b>E-Mail unzustellbar</b> %{submitter_name}'
@ -7587,7 +7581,6 @@ nl: &nl
embed: Insluiten embed: Insluiten
invite: Uitnodiging invite: Uitnodiging
link: Link link: Link
mcp: MCP
submission_event_names: submission_event_names:
send_email_to_html: "<b>E-mail verzonden</b> naar %{submitter_name}" send_email_to_html: "<b>E-mail verzonden</b> naar %{submitter_name}"
bounce_email_html: "<b>E-mail gebounced</b> %{submitter_name}" bounce_email_html: "<b>E-mail gebounced</b> %{submitter_name}"

@ -77,7 +77,7 @@ Rails.application.routes.draw do
resources :console_redirect, only: %i[index] resources :console_redirect, only: %i[index]
resources :upgrade, only: %i[index], controller: 'console_redirect' resources :upgrade, only: %i[index], controller: 'console_redirect'
resources :manage, only: %i[index], controller: 'console_redirect' resources :manage, only: %i[index], controller: 'console_redirect'
resource :testing_account, only: %i[create destroy] resource :testing_account, only: %i[show destroy]
resources :testing_api_settings, only: %i[index] resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index] resources :submitters_autocomplete, only: %i[index]
resources :submitters_resubmit, only: %i[update] resources :submitters_resubmit, only: %i[update]

@ -27,7 +27,7 @@ module Mcp
readOnlyHint: false, readOnlyHint: false,
destructiveHint: false, destructiveHint: false,
idempotentHint: false, idempotentHint: false,
openWorldHint: true openWorldHint: false
} }
}.freeze }.freeze
@ -43,7 +43,6 @@ module Mcp
account:, account:,
author: current_user, author: current_user,
folder: account.default_template_folder, folder: account.default_template_folder,
source: :mcp,
name: arguments['name'].to_s.presence || 'New Template', name: arguments['name'].to_s.presence || 'New Template',
fields: [], fields: [],
schema: [] schema: []

@ -61,7 +61,7 @@ module Mcp
}, },
annotations: { annotations: {
readOnlyHint: false, readOnlyHint: false,
destructiveHint: true, destructiveHint: false,
idempotentHint: false, idempotentHint: false,
openWorldHint: true openWorldHint: true
} }
@ -96,7 +96,7 @@ module Mcp
submissions = Submissions.create_from_submitters( submissions = Submissions.create_from_submitters(
template:, template:,
user: current_user, user: current_user,
source: :mcp, source: :api,
submitters_order: 'random', submitters_order: 'random',
submissions_attrs: { submitters: submitters }, submissions_attrs: { submitters: submitters },
params: { 'send_email' => true, 'submitters' => submitters } params: { 'send_email' => true, 'submitters' => submitters }

@ -54,7 +54,7 @@ Puma::Plugin.create do
Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty? Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty?
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"), exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
'--loglevel', 'warning') out: '/dev/null')
end end
end end

@ -56,8 +56,7 @@ module SearchEntries
end end
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword] [sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
elsif keyword.start_with?('@') || keyword.match?(/[^\p{L}\d&@.-]/) || elsif keyword.match?(/[^\p{L}\d&@.-]/) || keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.-]{2,}/)
keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.-]{2,}/)
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)] ['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
else else
keyword = TextUtils.transliterate(keyword.downcase).squish keyword = TextUtils.transliterate(keyword.downcase).squish

@ -162,7 +162,7 @@ module Submissions
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i) return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
return email.downcase if email.include?(',') || return email.downcase if email.include?(',') ||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) || email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
email.exclude?('.') email.exclude?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<')) fixed_email = EmailTypo.call(email.delete_prefix('<'))

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Templates module Templates
COLOR_REGEXP = /\A(#(?:[0-9a-f]{3}|[0-9a-f]{6})|[a-z]+)\z/i
EXPIRATION_DURATIONS = { EXPIRATION_DURATIONS = {
one_day: 1.day, one_day: 1.day,
two_days: 2.days, two_days: 2.days,

@ -6,7 +6,7 @@ module Templates
# rubocop:disable Metrics # rubocop:disable Metrics
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil) def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
template = author.account.templates.new template = original_template.account.templates.new
template.external_id = external_id template.external_id = external_id
template.shared_link = original_template.shared_link template.shared_link = original_template.shared_link
@ -16,10 +16,8 @@ module Templates
if folder_name.present? if folder_name.present?
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) template.folder = TemplateFolders.find_or_create_by_name(author, folder_name)
elsif author.account_id == original_template.account_id
template.folder_id = original_template.folder_id
else else
template.folder = author.account.default_template_folder template.folder_id = original_template.folder_id
end end
template.submitters, template.fields, template.schema, template.preferences = template.submitters, template.fields, template.schema, template.preferences =

Loading…
Cancel
Save