diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 4bcd3237..eb216bc5 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController Submissions::EnsureResultGenerated.call(last_submitter) - if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter) - Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) + if !signature_valid && !current_user_submitter?(last_submitter) + return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) - return head :not_found + if last_submitter.completed_at < TTL.ago + Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) + + return head :not_found + end end if params[:combined] == 'true' - url = build_combined_url(@submitter) - - if url - render json: [url] - else - head :not_found - end + respond_with_combined(last_submitter) else render json: build_urls(last_submitter) end @@ -48,8 +46,18 @@ class SubmissionsDownloadController < ApplicationController private + def respond_with_combined(submitter) + url = build_combined_url(submitter) + + if url + render json: [url] + else + head :not_found + end + end + def current_user_submitter?(submitter) - current_user && current_user.account.submitters.exists?(id: submitter.id) + current_user && current_ability.can?(:read, submitter) end def build_urls(submitter) diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index f723aa32..5a6a0ae5 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -9,15 +9,15 @@ class SubmitFormController < ApplicationController before_action :load_submitter, only: %i[show update completed] before_action :maybe_render_locked_page, only: :show - before_action :maybe_require_link_2fa, only: %i[show update] + before_action :maybe_require_link_2fa, only: %i[show] CONFIG_KEYS = [].freeze def show submission = @submitter.submission + return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request) return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? - return render :email_2fa if require_email_2fa?(@submitter) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @@ -48,7 +48,7 @@ class SubmitFormController < ApplicationController end def update - if require_email_2fa?(@submitter) + unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') }, status: :unprocessable_content end @@ -84,7 +84,9 @@ class SubmitFormController < ApplicationController def completed raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at? - redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter) + return if Submitters::AuthorizedForForm.call(@submitter, current_user, request) + + redirect_to submit_form_path(params[:submit_form_slug]) end def success; end @@ -92,10 +94,7 @@ class SubmitFormController < ApplicationController private def maybe_require_link_2fa - return if @submitter.submission.source != 'link' - return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true - return if cookies.encrypted[:email_2fa_slug] == @submitter.slug - return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id + return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request) redirect_to start_form_path(@submitter.submission.template.slug) end @@ -117,12 +116,4 @@ class SubmitFormController < ApplicationController ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) .preload(:blob).index_by(&:uuid) end - - def require_email_2fa?(submitter) - return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true && - submitter.preferences['require_email_2fa'] != true - return false if cookies.encrypted[:email_2fa_slug] == submitter.slug - - true - end end diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index 918903fe..a8f969c3 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -11,7 +11,9 @@ class SubmitFormDeclineController < ApplicationController submitter.completed_at? || submitter.submission.archived_at? || submitter.submission.expired? || - submitter.submission.template&.archived_at? + submitter.submission.template&.archived_at? || + !Submitters::AuthorizedForForm.call(submitter, current_user, + request) ApplicationRecord.transaction do submitter.update!(declined_at: Time.current) diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index d6e0b692..3ebdc5e2 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController @submitter.submission.template&.archived_at? || AccountConfig.exists?(account_id: @submitter.account_id, key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, - value: false) + value: false) || + !Submitters::AuthorizedForForm.call(@submitter, current_user, request) last_completed_submitter = @submitter.submission.submitters .where.not(id: @submitter.id) diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb index 773eb9e7..5ba141c1 100644 --- a/app/controllers/submit_form_draw_signature_controller.rb +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? - if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? + if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? || + !Submitters::AuthorizedForForm.call(@submitter, current_user, request) return redirect_to submit_form_path(@submitter.slug) end diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index ab1f26c3..bcc848ae 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -45,7 +45,8 @@ class SubmitFormInviteController < ApplicationController !submitter.completed_at? && !submitter.submission.archived_at? && !submitter.submission.expired? && - !submitter.submission.template&.archived_at? + !submitter.submission.template&.archived_at? && + Submitters::AuthorizedForForm.call(submitter, current_user, request) end def filter_invite_submitters(submitter, key = 'invite_by_uuid') diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb index e1a6b9ab..affd37ba 100644 --- a/app/controllers/submit_form_values_controller.rb +++ b/app/controllers/submit_form_values_controller.rb @@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController def index submitter = Submitter.find_by!(slug: params[:submit_form_slug]) - return render json: {} if submitter.completed_at? || submitter.declined_at? - return render json: {} if submitter.submission.template&.archived_at? || + return render json: {} if submitter.completed_at? || + submitter.declined_at? || + submitter.submission.template&.archived_at? || submitter.submission.archived_at? || - submitter.submission.expired? + submitter.submission.expired? || + !Submitters::AuthorizedForForm.call(submitter, current_user, request) value = submitter.values[params['field_uuid']] attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index e8c00aea..ddee4e90 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -56,7 +56,7 @@ class TemplatesUploadsController < ApplicationController def create_file_params_from_url tempfile = Tempfile.new tempfile.binmode - tempfile.write(DownloadUtils.call(params[:url]).body) + tempfile.write(DownloadUtils.call(params[:url], validate: true).body) tempfile.rewind filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present? diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue index 708843bf..c87f9c4f 100644 --- a/app/javascript/submission_form/completed.vue +++ b/app/javascript/submission_form/completed.vue @@ -161,6 +161,11 @@ export default { required: false, default: false }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, completedButton: { type: Object, required: false, @@ -214,7 +219,10 @@ export default { download () { this.isDownloading = true - fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => { + fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, { + method: 'GET', + ...this.fetchOptions + }).then(async (response) => { if (response.ok) { const urls = await response.json() const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent) diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index e3a3fadd..e2bdcf42 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -530,6 +530,7 @@ v-else-if="isInvite" :submitters="inviteSubmitters" :optional-submitters="optionalInviteSubmitters" + :fetch-options="fetchOptions" :submitter-slug="submitterSlug" :authenticity-token="authenticityToken" :url="baseUrl + submitPath + '/invite'" @@ -543,6 +544,7 @@ :has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))" :has-multiple-documents="hasMultipleDocuments" :completed-button="completedRedirectUrl ? {} : completedButton" + :fetch-options="fetchOptions" :completed-message="completedRedirectUrl ? {} : completedMessage" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl" :with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun" @@ -678,6 +680,11 @@ export default { required: false, default: () => [] }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, optionalInviteSubmitters: { type: Array, required: false, @@ -1467,7 +1474,8 @@ export default { } else { return fetch(this.baseUrl + this.submitPath, { method: 'POST', - body: formData || new FormData(this.$refs.form) + body: formData || new FormData(this.$refs.form), + ...this.fetchOptions }).then((response) => { if (response.status === 200) { currentFieldUuids.forEach((fieldUuid) => { diff --git a/app/javascript/submission_form/invite_form.vue b/app/javascript/submission_form/invite_form.vue index 10c3927d..3189b0d8 100644 --- a/app/javascript/submission_form/invite_form.vue +++ b/app/javascript/submission_form/invite_form.vue @@ -78,6 +78,11 @@ export default { type: Array, required: true }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, optionalSubmitters: { type: Array, required: false, @@ -108,7 +113,8 @@ export default { return fetch(this.url, { method: 'POST', - body: new FormData(this.$refs.form) + body: new FormData(this.$refs.form), + ...this.fetchOptions }).then((response) => { if (response.status === 200) { this.$emit('success') diff --git a/app/javascript/template_builder/page.vue b/app/javascript/template_builder/page.vue index 550658c4..8580198c 100644 --- a/app/javascript/template_builder/page.vue +++ b/app/javascript/template_builder/page.vue @@ -261,7 +261,7 @@ export default { }, computed: { isSelectMode () { - return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField + return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField && !this.drawCustomField }, pageSelectedAreas () { if (!this.selectedAreasRef.value) return [] diff --git a/app/jobs/send_test_webhook_request_job.rb b/app/jobs/send_test_webhook_request_job.rb index 158363ec..0116cdaf 100644 --- a/app/jobs/send_test_webhook_request_job.rb +++ b/app/jobs/send_test_webhook_request_job.rb @@ -26,7 +26,7 @@ class SendTestWebhookRequestJob Addressable::URI.parse(webhook_url.url).normalize end - raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' + raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || [443, nil].exclude?(uri.port) raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS) end diff --git a/app/views/email_smtp_settings/index.html.erb b/app/views/email_smtp_settings/index.html.erb index 7e556a47..821d8335 100644 --- a/app/views/email_smtp_settings/index.html.erb +++ b/app/views/email_smtp_settings/index.html.erb @@ -22,7 +22,7 @@
<%= ff.label :password, 'Password', class: 'label' %> - <%= ff.password_field :password, value: value['password'], class: 'base-input' %> + <%= ff.password_field :password, class: 'base-input', required: value['password'].present?, placeholder: value['password'].present? ? '*************' : '' %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 89646c06..5558e869 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -22,7 +22,7 @@ <% if params[:modal].present? %> <% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %> <% if url_params[:action] == 'new' %> - + <% end %> <% end %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb index d7043120..68022061 100644 --- a/app/views/pwa/manifest.json.erb +++ b/app/views/pwa/manifest.json.erb @@ -4,7 +4,7 @@ "id": "/", "icons": [ { - "src": "/logo.svg", + "src": "/favicon.svg", "type": "image/svg+xml", "sizes": "any" }, diff --git a/app/views/shared/_meta.html.erb b/app/views/shared/_meta.html.erb index 4a9c201f..a6d492f9 100644 --- a/app/views/shared/_meta.html.erb +++ b/app/views/shared/_meta.html.erb @@ -27,4 +27,5 @@ + diff --git a/app/views/shared/_search_input.html.erb b/app/views/shared/_search_input.html.erb index 78f8208d..8457b2af 100644 --- a/app/views/shared/_search_input.html.erb +++ b/app/views/shared/_search_input.html.erb @@ -6,7 +6,7 @@ <% end %> <% if params[:q].present? %>
- + ×
diff --git a/app/views/storage_settings/_aws_form.html.erb b/app/views/storage_settings/_aws_form.html.erb index b08fd468..e90077dd 100644 --- a/app/views/storage_settings/_aws_form.html.erb +++ b/app/views/storage_settings/_aws_form.html.erb @@ -8,7 +8,7 @@
<%= fff.label :secret_access_key, class: 'label' %> - <%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true, class: 'base-input' %> + <%= fff.password_field :secret_access_key, required: true, class: 'base-input', placeholder: configs['secret_access_key'].present? ? '*************' : '' %>
diff --git a/app/views/storage_settings/_azure_form.html.erb b/app/views/storage_settings/_azure_form.html.erb index 22901b7c..00a95be9 100644 --- a/app/views/storage_settings/_azure_form.html.erb +++ b/app/views/storage_settings/_azure_form.html.erb @@ -13,7 +13,7 @@
<%= fff.label :storage_access_key, 'Storage Access Key', class: 'label' %> - <%= fff.password_field :storage_access_key, value: configs['storage_access_key'], required: true, class: 'base-input' %> + <%= fff.password_field :storage_access_key, required: true, class: 'base-input', placeholder: configs['storage_access_key'].present? ? '*************' : '' %>
<% end %> <% end %> diff --git a/app/views/storage_settings/_google_cloud_form.html.erb b/app/views/storage_settings/_google_cloud_form.html.erb index 11ce0399..3b8531f6 100644 --- a/app/views/storage_settings/_google_cloud_form.html.erb +++ b/app/views/storage_settings/_google_cloud_form.html.erb @@ -13,7 +13,7 @@
<%= fff.label :credentials, 'Credentials (JSON key content)', class: 'label' %> - <%= fff.text_area :credentials, value: configs['credentials'], required: true, class: 'base-textarea w-full font-mono', rows: 4 %> + <%= fff.text_area :credentials, required: true, class: 'base-textarea w-full font-mono', rows: 4, placeholder: configs['credentials'].present? ? "{\n**REDACTED**\n}" : '' %>
<% end %> <% end %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index a40d0c9a..5d19f351 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -68,7 +68,7 @@ <% end %> <% end %> - <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %> + <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %> <% end %> @@ -159,7 +159,7 @@ <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> - <% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %> + <% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %> <%= svg_icon('pencil', class: 'w-5 h-5') %> @@ -225,15 +225,15 @@ <% end %> - <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %> + <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
<% end %> - <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %> + <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %> <%= render 'submissions/send_sms_button', submitter: %> <% end %> - <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %> + <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
<%= t('sign_in_person') %> diff --git a/app/views/submissions_dashboard/index.html.erb b/app/views/submissions_dashboard/index.html.erb index 286e0b91..d61943d6 100644 --- a/app/views/submissions_dashboard/index.html.erb +++ b/app/views/submissions_dashboard/index.html.erb @@ -35,19 +35,19 @@ <% if is_show_tabs %>
@@ -27,7 +27,7 @@ <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %> <%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %> <% end %> - <%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> <%= svg_icon('x', class: 'w-5 h-5') %> <% end %>
@@ -46,7 +46,7 @@ <% end %> <% end %> - <%= link_to url_for(params.to_unsafe_h.except(:completed_at_from, :completed_at_to)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> <%= svg_icon('x', class: 'w-5 h-5') %> <% end %>
@@ -65,7 +65,7 @@ <% end %> <% end %> - <%= link_to url_for(params.to_unsafe_h.except(:created_at_to, :created_at_from)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> <%= svg_icon('x', class: 'w-5 h-5') %> <% end %> diff --git a/app/views/submissions_filters/_filter_modal.html.erb b/app/views/submissions_filters/_filter_modal.html.erb index af4a9b86..8794197a 100644 --- a/app/views/submissions_filters/_filter_modal.html.erb +++ b/app/views/submissions_filters/_filter_modal.html.erb @@ -10,7 +10,7 @@ <% if params[:with_remove] %>
- <%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %> + <%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
<% end %> <% end %> diff --git a/app/views/templates/show.html.erb b/app/views/templates/show.html.erb index 82ffb69e..65ee7717 100644 --- a/app/views/templates/show.html.erb +++ b/app/views/templates/show.html.erb @@ -30,7 +30,7 @@ <% if is_show_tabs %>
- +
<%= svg_icon('list', class: 'w-5 h-5') %> <%= t('all') %> @@ -41,7 +41,7 @@
<% end %>
- +
<%= svg_icon('clock', class: 'w-5 h-5') %> <%= t('pending') %> @@ -52,7 +52,7 @@
<% end %>
- +
<%= svg_icon('circle_check', class: 'w-5 h-5') %> <%= t('completed') %> diff --git a/app/views/webhook_settings/show.html.erb b/app/views/webhook_settings/show.html.erb index 37a15640..a8f16dd5 100644 --- a/app/views/webhook_settings/show.html.erb +++ b/app/views/webhook_settings/show.html.erb @@ -85,9 +85,9 @@

<%= t('events_log') %>

- <%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> - <%= link_to t('succeeded'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> - <%= link_to t('failed'), url_for(params.to_unsafe_h.merge(status: 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> + <%= link_to t('all'), url_for(params: request.query_parameters.except('status')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> + <%= link_to t('succeeded'), url_for(params: request.query_parameters.merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> + <%= link_to t('failed'), url_for(params: request.query_parameters.merge('status' => 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<% if @webhook_events.present? %>
diff --git a/config/routes.rb b/config/routes.rb index 4447a239..2703f136 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,7 +56,7 @@ Rails.application.routes.draw do resources :account_custom_fields, only: %i[create] resources :user_configs, only: %i[create] resources :encrypted_user_configs, only: %i[destroy] - resources :timestamp_server, only: %i[create] + resources :timestamp_server, only: %i[create] unless Docuseal.multitenant? resources :dashboard, only: %i[index] resources :setup, only: %i[index create] resource :newsletter, only: %i[show update] diff --git a/lib/download_utils.rb b/lib/download_utils.rb index dce5427a..8b352502 100644 --- a/lib/download_utils.rb +++ b/lib/download_utils.rb @@ -35,16 +35,16 @@ module DownloadUtils module_function - def call(url) + def call(url, validate: Docuseal.multitenant?) uri = begin URI(url) rescue URI::Error Addressable::URI.parse(url).normalize end - validate_uri!(uri) if Docuseal.multitenant? + validate_uri!(uri) if validate - resp = conn.get(uri) + resp = conn(validate:).get(uri) raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400 @@ -52,14 +52,15 @@ module DownloadUtils end def validate_uri!(uri) - raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' + raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' || + [443, nil].exclude?(uri.port) raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS) end - def conn + def conn(validate: Docuseal.multitenant?) Faraday.new do |faraday| faraday.response :follow_redirects, callback: lambda { |_, new_env| - validate_uri!(new_env[:url]) if Docuseal.multitenant? + validate_uri!(new_env[:url]) if validate } end end diff --git a/lib/send_webhook_request.rb b/lib/send_webhook_request.rb index a3474eaf..87de376a 100644 --- a/lib/send_webhook_request.rb +++ b/lib/send_webhook_request.rb @@ -22,7 +22,7 @@ module SendWebhookRequest end if Docuseal.multitenant? - raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' && + raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || [443, nil].exclude?(uri.port)) && !AccountConfig.exists?(key: :allow_http, account_id: webhook_url.account_id) raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS) diff --git a/lib/submissions.rb b/lib/submissions.rb index aaf18ea4..8fd26bf0 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -18,7 +18,8 @@ module Submissions def plain_search(submissions, keyword, search_values: false, search_template: false) return submissions if keyword.blank? - term = "%#{keyword.downcase}%" + sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase) + term = "%#{sanitized}%" arel_table = Submitter.arel_table @@ -31,7 +32,7 @@ module Submissions if search_template submissions = submissions.left_joins(:template) - arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%")) + arel = arel.or(Template.arel_table[:name].lower.matches("%#{sanitized}%")) end submissions.joins(:submitters).where(arel).group(:id) diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 72eeb9f1..580b9be1 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -162,7 +162,7 @@ module Submissions pdf.trailer.info[:DocumentID] = document_id pdf.pages.each do |page| - font_size = (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i + font_size = [(([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i, 4].max cnv = page.canvas(type: :overlay) text = diff --git a/lib/submitters.rb b/lib/submitters.rb index c557dd3e..47fbf014 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -14,6 +14,7 @@ module Submitters UnableToSendCode = Class.new(StandardError) InvalidOtp = Class.new(StandardError) MaliciousFileExtension = Class.new(StandardError) + ArgumentError = Class.new(StandardError) DANGEROUS_EXTENSIONS = Set.new(%w[ exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp @@ -133,7 +134,7 @@ module Submitters filename: file.original_filename, content_type: file.content_type) else - ActiveStorage::Blob.find_signed(params[:blob_signed_id]) + raise ArgumentError, 'file param is missing' end ActiveStorage::Attachment.create!( diff --git a/lib/submitters/authorized_for_form.rb b/lib/submitters/authorized_for_form.rb new file mode 100644 index 00000000..81048a16 --- /dev/null +++ b/lib/submitters/authorized_for_form.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Submitters + module AuthorizedForForm + Unauthorized = Class.new(StandardError) + + module_function + + def call(submitter, current_user, request) + pass_email_2fa?(submitter, request) && pass_link_2fa?(submitter, current_user, request) + end + + def pass_email_2fa?(submitter, request) + return false unless submitter + + return true if submitter.submission.template&.preferences&.dig('require_email_2fa') != true && + submitter.preferences['require_email_2fa'] != true + return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug + + token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence + + return true if token.present? && + Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == submitter.slug + + false + end + + def pass_link_2fa?(submitter, current_user, request) + return false unless submitter + + return true if submitter.submission.source != 'link' + return true unless submitter.submission.template&.preferences&.dig('shared_link_2fa') == true + return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug + return true if submitter.email == current_user&.email && current_user&.account_id == submitter.account_id + + if (token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence) + link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':') + + return true if Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == link_2fa_key + end + + false + end + end +end diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb index 16cb268c..1eee3dfd 100644 --- a/lib/submitters/normalize_values.rb +++ b/lib/submitters/normalize_values.rb @@ -236,7 +236,7 @@ module Submitters return blob if blob - data = DownloadUtils.call(url).body + data = DownloadUtils.call(url, validate: true).body checksum = Digest::MD5.base64digest(data) diff --git a/lib/template_folders.rb b/lib/template_folders.rb index 00a6fc02..d4a3af9e 100644 --- a/lib/template_folders.rb +++ b/lib/template_folders.rb @@ -20,7 +20,9 @@ module TemplateFolders def search(folders, keyword) return folders if keyword.blank? - folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%")) + sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase) + + folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{sanitized}%")) end def filter_active_folders(template_folders, templates) diff --git a/lib/templates.rb b/lib/templates.rb index 73aaef80..2abd93dd 100644 --- a/lib/templates.rb +++ b/lib/templates.rb @@ -52,7 +52,9 @@ module Templates def plain_search(templates, keyword) return templates if keyword.blank? - templates.where(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%")) + sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase) + + templates.where(Template.arel_table[:name].lower.matches("%#{sanitized}%")) end def fulltext_search(current_user, templates, keyword) diff --git a/public/apple-icon-180x180.png b/public/apple-icon-180x180.png index 54ae5fcf..08c0c3b8 100644 Binary files a/public/apple-icon-180x180.png and b/public/apple-icon-180x180.png differ diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png index d6a6d23c..67113d91 100644 Binary files a/public/apple-touch-icon-precomposed.png and b/public/apple-touch-icon-precomposed.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index d6a6d23c..67113d91 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png index 45a2768f..e38921b8 100644 Binary files a/public/favicon-16x16.png and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png index 01e7fada..fd8ea8dd 100644 Binary files a/public/favicon-32x32.png and b/public/favicon-32x32.png differ diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png index a1eee746..600a59f4 100644 Binary files a/public/favicon-96x96.png and b/public/favicon-96x96.png differ diff --git a/public/favicon.ico b/public/favicon.ico index cb036abd..c0c2bf89 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 00000000..ebfb3040 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb index b60a3d9b..6eb7cdf8 100644 --- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ RSpec.describe SendFormCompletedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb index 8e9d2d0d..99f26eef 100644 --- a/spec/jobs/send_form_declined_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb index e09f4205..54a5c521 100644 --- a/spec/jobs/send_form_started_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ RSpec.describe SendFormStartedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb index 31026341..5cbed3c3 100644 --- a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ RSpec.describe SendFormViewedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb index 97ec0b69..72764b04 100644 --- a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb @@ -13,6 +13,10 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb index e80b97a6..62a1d432 100644 --- a/spec/jobs/send_submission_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb @@ -13,6 +13,10 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_submission_expired_webhook_request_job_spec.rb b/spec/jobs/send_submission_expired_webhook_request_job_spec.rb index dbce55ba..541eb73b 100644 --- a/spec/jobs/send_submission_expired_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_expired_webhook_request_job_spec.rb @@ -13,6 +13,10 @@ RSpec.describe SendSubmissionExpiredWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb index e5ce6f10..696d47d4 100644 --- a/spec/jobs/send_template_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb @@ -12,6 +12,10 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb index f13675a1..c0ecec8b 100644 --- a/spec/jobs/send_template_updated_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb @@ -12,6 +12,10 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index db91da56..ecca6019 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -54,6 +54,7 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods config.include Devise::Test::IntegrationHelpers config.include SigningFormHelper + config.include ActiveSupport::Testing::TimeHelpers config.before(:each, type: :system) do if ENV['HEADLESS'] == 'false' diff --git a/spec/system/email_settings_spec.rb b/spec/system/email_settings_spec.rb index f7d683b8..fe842a1d 100644 --- a/spec/system/email_settings_spec.rb +++ b/spec/system/email_settings_spec.rb @@ -61,7 +61,6 @@ RSpec.describe 'Email Settings' do expect(page).to have_field('Host', with: encrypted_config.value['host']) expect(page).to have_field('Port', with: encrypted_config.value['port']) expect(page).to have_field('Username', with: encrypted_config.value['username']) - expect(page).to have_field('Password', with: encrypted_config.value['password']) expect(page).to have_field('Domain', with: encrypted_config.value['domain']) expect(page).to have_select('Authentication', selected: 'Plain') expect(page).to have_field('Send from Email', with: encrypted_config.value['from_email'])