diff --git a/app/controllers/notifications_settings_controller.rb b/app/controllers/notifications_settings_controller.rb
index f03f09c8..83502acd 100644
--- a/app/controllers/notifications_settings_controller.rb
+++ b/app/controllers/notifications_settings_controller.rb
@@ -8,6 +8,7 @@ class NotificationsSettingsController < ApplicationController
before_action :build_account_config, only: :create
authorize_resource :account_config, only: :create
+ before_action :authorize_email_reminders!, only: :create
def index; end
@@ -21,6 +22,14 @@ class NotificationsSettingsController < ApplicationController
private
+ def authorize_email_reminders!
+ return unless Docuseal.multitenant?
+ return if @account_config.key != AccountConfig::SUBMITTER_REMINDERS
+ return if can?(:manage, :email_reminders)
+
+ redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unlock_with_docuseal_pro')
+ end
+
def build_account_config
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key])
diff --git a/app/controllers/preview_document_page_controller.rb b/app/controllers/preview_document_page_controller.rb
index 5f5a8554..05b848da 100644
--- a/app/controllers/preview_document_page_controller.rb
+++ b/app/controllers/preview_document_page_controller.rb
@@ -5,6 +5,10 @@ class PreviewDocumentPageController < ActionController::API
FORMAT = Templates::ProcessDocument::FORMAT
+ TMPFILE_PREFIX = 'attachment-'
+ TMPFILE_TTL = 5.minutes
+ TMPFILE_MAX_TOTAL_SIZE = Docuseal.multitenant? ? 400.megabytes : 1.gigabyte
+
def show
result_data =
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
@@ -30,34 +34,63 @@ class PreviewDocumentPageController < ActionController::API
allow_other_host: true
end
- file_path =
- if attachment.service.name == :disk
- ActiveStorage::Blob.service.path_for(attachment.key)
- else
- find_or_create_document_tempfile_path(attachment)
- end
-
preview_image =
- Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
+ open_attachment_io(attachment) do |io|
+ Templates::ProcessDocument.generate_pdf_preview_from_io(attachment, io, params[:id].to_i)
+ end
redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true
end
- def find_or_create_document_tempfile_path(attachment)
- file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
+ def open_attachment_io(attachment, &)
+ return File.open(ActiveStorage::Blob.service.path_for(attachment.key), 'rb', &) if attachment.service.name == :disk
- File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
- f.flock(File::LOCK_EX)
+ file_name = "#{TMPFILE_PREFIX}#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
+
+ file_path = File.join(Dir.tmpdir, file_name)
+
+ File.open(file_path, File::RDWR | File::CREAT, 0o644) do |file|
+ file.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
- if f.size.zero?
- f.binmode
+ if file.size.zero?
+ cleanup_stale_tempfiles
- f.write(attachment.download)
+ file.binmode
+
+ file.write(attachment.download)
+ else
+ FileUtils.touch(file_path)
end
# rubocop:enable Style/ZeroLengthPredicate
- end
- file_path
+ file.flock(File::LOCK_UN)
+
+ yield file
+ end
+ end
+
+ def cleanup_stale_tempfiles
+ entries =
+ Dir.glob(File.join(Dir.tmpdir, "#{TMPFILE_PREFIX}*")).filter_map do |path|
+ stat = File.stat(path)
+
+ [path, stat.mtime, stat.size]
+ rescue Errno::ENOENT
+ nil
+ end
+
+ total_size = entries.sum(&:last)
+ stale_time = TMPFILE_TTL.ago
+
+ entries.sort_by(&:second).each do |path, mtime, size|
+ break if mtime > stale_time && total_size <= TMPFILE_MAX_TOTAL_SIZE
+
+ File.unlink(path)
+
+ total_size -= size
+ rescue Errno::ENOENT
+ nil
+ end
end
end
diff --git a/app/controllers/start_form_email_2fa_send_controller.rb b/app/controllers/start_form_email_2fa_send_controller.rb
index 6359debd..eb20dfc7 100644
--- a/app/controllers/start_form_email_2fa_send_controller.rb
+++ b/app/controllers/start_form_email_2fa_send_controller.rb
@@ -6,9 +6,10 @@ class StartFormEmail2faSendController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
- def create
- @template = Template.find_by!(slug: params[:slug])
+ before_action :load_template
+ before_action :authorize_start!
+ def create
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id)
@@ -25,6 +26,23 @@ class StartFormEmail2faSendController < ApplicationController
private
+ def load_template
+ @template = Template.find_by!(slug: params[:slug])
+ end
+
+ def authorize_start!
+ is_archived = @template.archived_at? || @template.account.archived_at?
+
+ return redirect_to start_form_path(@template.slug) if is_archived
+
+ return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
+ @template.preferences['shared_link_2fa'] == true
+
+ Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
+
+ redirect_to start_form_path(@template.slug)
+ end
+
def submitter_params
params.require(:submitter).permit(:name, :email, :phone)
end
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index 96ae4972..0f04c0df 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -40,7 +40,7 @@ class SubmissionsController < ApplicationController
def create
return redirect_to template_path(@template), alert: I18n.t('template_has_been_archived') if @template.archived_at?
- save_template_message(@template, params) if params[:save_message] == '1'
+ save_template_message(@template, params) if params[:save_message] == '1' && can?(:update, @template)
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
diff --git a/app/controllers/submit_form_completed_download_controller.rb b/app/controllers/submit_form_completed_download_controller.rb
index 6f959d0d..17503778 100644
--- a/app/controllers/submit_form_completed_download_controller.rb
+++ b/app/controllers/submit_form_completed_download_controller.rb
@@ -10,18 +10,19 @@ class SubmitFormCompletedDownloadController < ApplicationController
def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
- signature_valid =
- if @submitter&.slug == submitter_slug
- true
- else
- @submitter = nil
- end
+ signature_valid = @submitter&.slug == submitter_slug
- @submitter ||= Submitter.find_by!(slug: submitter_slug)
+ @submitter = Submitter.find_by!(slug: submitter_slug) unless signature_valid
+
+ unless completed_submitter?(@submitter)
+ Rollbar.error("Not completed: #{@submitter.id}") if defined?(Rollbar)
+
+ return head :not_found
+ end
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
- last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
+ last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last
return head :not_found unless last_submitter
@@ -60,6 +61,10 @@ class SubmitFormCompletedDownloadController < ApplicationController
end
end
+ def completed_submitter?(submitter)
+ submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
+ end
+
def current_user_submitter?(submitter)
current_user && current_ability.can?(:read, submitter)
end
diff --git a/app/controllers/submitters_autocomplete_controller.rb b/app/controllers/submitters_autocomplete_controller.rb
index 22599e58..f93c8b96 100644
--- a/app/controllers/submitters_autocomplete_controller.rb
+++ b/app/controllers/submitters_autocomplete_controller.rb
@@ -36,7 +36,7 @@ class SubmittersAutocompleteController < ApplicationController
else
column = Submitter.arel_table[field.to_sym]
- term = "#{params[:q].downcase}%"
+ term = "#{ActiveRecord::Base.sanitize_sql_like(params[:q].downcase)}%"
submitters.where(column.matches(term))
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 2d8f818f..0b611381 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -75,6 +75,8 @@ class UsersController < ApplicationController
authorize!(:manage, account)
@user.account = account
+
+ authorize!(:create, @user)
end
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue
index d73aee07..648b8a44 100644
--- a/app/javascript/submission_form/area.vue
+++ b/app/javascript/submission_form/area.vue
@@ -101,7 +101,7 @@
- {{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
+ {{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
diff --git a/app/javascript/template_builder/dynamic_variables_schema.js b/app/javascript/template_builder/dynamic_variables_schema.js
index f085b0b4..64869bc3 100644
--- a/app/javascript/template_builder/dynamic_variables_schema.js
+++ b/app/javascript/template_builder/dynamic_variables_schema.js
@@ -417,10 +417,8 @@ function buildOperators (tags) {
break
}
case 'end': {
- const popped = stack.pop()
-
- if (popped.operator) {
- popped.operator.endTag = tag
+ if (stack.length > 1) {
+ stack.pop().operator.endTag = tag
}
break
@@ -434,7 +432,13 @@ function buildOperators (tags) {
return operators
}
+function hasBlankKey (keyString) {
+ return !keyString || keyString.split('.').some((key) => !key.trim())
+}
+
function assignNestedSchema (propertiesHash, parentProperties, keyString, value) {
+ if (hasBlankKey(keyString)) return
+
const keys = keyString.split('.')
const lastKey = keys.pop()
@@ -460,6 +464,8 @@ function assignNestedSchema (propertiesHash, parentProperties, keyString, value)
}
function assignNestedSchemaWithPriority (propertiesHash, parentProperties, keyString, newType) {
+ if (hasBlankKey(keyString)) return
+
const keys = keyString.split('.')
const lastKey = keys.pop()
@@ -523,6 +529,8 @@ function processOperators (operators, propertiesHash = {}, parentProperties = {}
processOperators(op.elseChildren, propertiesHash, parentProperties)
break
case 'for': {
+ if (hasBlankKey(op.variableName)) break
+
const parts = op.variableName.split('.')
const singularKey = singularize(parts[parts.length - 1])
diff --git a/app/views/submissions/_send_email_base.html.erb b/app/views/submissions/_send_email_base.html.erb
index 4e640d70..24d25e73 100644
--- a/app/views/submissions/_send_email_base.html.erb
+++ b/app/views/submissions/_send_email_base.html.erb
@@ -69,7 +69,7 @@
<%= f.label :message, t('body'), class: 'label' %>
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
<%= render 'personalization_settings/email_body_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_edit_viewer ? view_template_body : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || (is_edit_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
- <% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' %>
+ <% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' && template && can?(:update, template) %>