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