diff --git a/app/controllers/api/submitter_email_clicks_controller.rb b/app/controllers/api/submitter_email_clicks_controller.rb
index cef265422..9c52fc5b6 100644
--- a/app/controllers/api/submitter_email_clicks_controller.rb
+++ b/app/controllers/api/submitter_email_clicks_controller.rb
@@ -1,10 +1,7 @@
# frozen_string_literal: true
module Api
- class SubmitterEmailClicksController < ApiBaseController
- skip_before_action :authenticate_user!
- skip_authorization_check
-
+ class SubmitterEmailClicksController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb
index d9ac441ef..263af0726 100644
--- a/app/controllers/api/submitter_form_views_controller.rb
+++ b/app/controllers/api/submitter_form_views_controller.rb
@@ -1,10 +1,7 @@
# frozen_string_literal: true
module Api
- class SubmitterFormViewsController < ApiBaseController
- skip_before_action :authenticate_user!
- skip_authorization_check
-
+ class SubmitterFormViewsController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
diff --git a/app/controllers/submit_form_click_email_controller.rb b/app/controllers/submit_form_click_email_controller.rb
new file mode 100644
index 000000000..bebacde43
--- /dev/null
+++ b/app/controllers/submit_form_click_email_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class SubmitFormClickEmailController < ActionController::API
+ def create
+ @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
+
+ if params[:t].present? && params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
+ SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
+ end
+
+ render json: {}
+ end
+end
diff --git a/app/controllers/submit_form_completed_download_controller.rb b/app/controllers/submit_form_completed_download_controller.rb
index 7e244fe66..180d6f294 100644
--- a/app/controllers/submit_form_completed_download_controller.rb
+++ b/app/controllers/submit_form_completed_download_controller.rb
@@ -29,7 +29,9 @@ class SubmitFormCompletedDownloadController < ApplicationController
Submissions::EnsureResultGenerated.call(last_submitter)
if !signature_valid && !current_user_submitter?(last_submitter)
- return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
+ unless Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
+ return head :not_found
+ end
if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
diff --git a/app/controllers/submit_form_delegate_controller.rb b/app/controllers/submit_form_delegate_controller.rb
index da0e4ed1e..b46dd8d57 100644
--- a/app/controllers/submit_form_delegate_controller.rb
+++ b/app/controllers/submit_form_delegate_controller.rb
@@ -15,7 +15,8 @@ class SubmitFormDelegateController < ApplicationController
@submitter.viewer? ||
!Submitters::AuthorizedForForm.call(@submitter,
current_user,
- request)
+ request,
+ with_order: true)
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb
index 821267b21..6758b2c7a 100644
--- a/app/controllers/submit_form_download_controller.rb
+++ b/app/controllers/submit_form_download_controller.rb
@@ -19,7 +19,8 @@ class SubmitFormDownloadController < ApplicationController
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false) ||
- !Submitters::AuthorizedForForm.call(@submitter, current_user, request)
+ !Submitters::AuthorizedForForm.call(@submitter, current_user, request,
+ with_order: true)
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 54faa7d2b..667fe42ad 100644
--- a/app/controllers/submit_form_draw_signature_controller.rb
+++ b/app/controllers/submit_form_draw_signature_controller.rb
@@ -15,7 +15,7 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
- !Submitters::AuthorizedForForm.call(@submitter, current_user, request)
+ !Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
return redirect_to submit_form_path(@submitter.slug)
end
diff --git a/app/controllers/submit_form_metadata_controller.rb b/app/controllers/submit_form_metadata_controller.rb
index 62a73d9f2..55c95c830 100644
--- a/app/controllers/submit_form_metadata_controller.rb
+++ b/app/controllers/submit_form_metadata_controller.rb
@@ -39,6 +39,6 @@ class SubmitFormMetadataController < ApplicationController
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at? &&
!submitter.account.archived_at? &&
- Submitters::AuthorizedForForm.call(submitter, current_user, request)
+ Submitters::AuthorizedForForm.call(submitter, current_user, request, with_order: true)
end
end
diff --git a/app/controllers/submit_form_upload_controller.rb b/app/controllers/submit_form_upload_controller.rb
new file mode 100644
index 000000000..e25f42207
--- /dev/null
+++ b/app/controllers/submit_form_upload_controller.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+class SubmitFormUploadController < ActionController::API
+ include ActionController::Cookies
+ include ActiveStorage::SetCurrent
+
+ before_action :load_submitter
+ before_action :authorize_upload!
+
+ COOKIE_STORE_LIMIT = 10
+
+ def create
+ file = params[:file]
+
+ if params[:type].in?(%w[initials signature])
+ image = ImageUtils.load_vips(file.read, content_type: file.content_type)
+
+ if ImageUtils.blank?(image)
+ Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
+
+ return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
+ end
+
+ if ImageUtils.error?(image)
+ Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
+
+ return render json: { error: "#{params[:type]} error, try to sign on another device" },
+ status: :unprocessable_content
+ end
+
+ metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
+ end
+
+ attachment = Submitters.create_attachment!(@submitter, file, metadata:)
+
+ if params[:remember_signature] == 'true' && @submitter.email.present?
+ cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
+ end
+
+ render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
+ rescue Submitters::MaliciousFileExtension => e
+ Rollbar.error(e) if defined?(Rollbar)
+
+ render json: { error: e.message }, status: :unprocessable_content
+ end
+
+ private
+
+ def load_submitter
+ @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
+ end
+
+ def authorize_upload!
+ unless can_upload?(@submitter)
+ return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
+ end
+
+ return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
+
+ render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
+ status: :unprocessable_content
+ end
+
+ def can_upload?(submitter)
+ !submitter.declined_at? &&
+ !submitter.completed_at? &&
+ !submitter.submission.archived_at? &&
+ !submitter.submission.expired? &&
+ !submitter.submission.template&.archived_at?
+ end
+
+ def build_new_cookie_signatures_json(submitter, attachment)
+ values =
+ begin
+ JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
+ rescue JSON::ParserError
+ {}
+ end
+
+ values[submitter.email] = attachment.uuid
+
+ values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
+
+ values.to_json
+ end
+end
diff --git a/app/controllers/submit_form_view_controller.rb b/app/controllers/submit_form_view_controller.rb
new file mode 100644
index 000000000..c5f31a1d6
--- /dev/null
+++ b/app/controllers/submit_form_view_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class SubmitFormViewController < ActionController::API
+ def create
+ @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
+
+ return head :not_found if Submitter.signed_id_verifier.verified(params[:v], purpose: :view_form) != @submitter.slug
+
+ @submitter.opened_at = Time.current
+ @submitter.save
+
+ SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
+
+ WebhookUrls.enqueue_events(@submitter, 'form.viewed')
+
+ render json: {}
+ end
+end
diff --git a/app/javascript/form.js b/app/javascript/form.js
index 83bce1a52..96aba8b45 100644
--- a/app/javascript/form.js
+++ b/app/javascript/form.js
@@ -41,6 +41,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
language: this.dataset.language,
paymentProvider: this.dataset.paymentProvider,
dryRun: this.dataset.dryRun === 'true',
+ viewToken: this.dataset.viewToken,
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
withSignatureId: this.dataset.withSignatureId === 'true',
requireSigningReason: this.dataset.requireSigningReason === 'true',
diff --git a/app/javascript/submission_form/dropzone.vue b/app/javascript/submission_form/dropzone.vue
index 95b287b61..c3fddf57e 100644
--- a/app/javascript/submission_form/dropzone.vue
+++ b/app/javascript/submission_form/dropzone.vue
@@ -87,7 +87,7 @@ export default {
IconCloudUpload,
IconInnerShadowTop
},
- inject: ['baseUrl', 't'],
+ inject: ['baseUrl', 'fetchOptions', 't'],
props: {
message: {
type: String,
@@ -199,6 +199,7 @@ export default {
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
+ headers: { ...this.fetchOptions.headers },
body: formData
}).then(async (resp) => {
const data = await resp.json()
diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue
index 0bb530634..a3cb83ad4 100644
--- a/app/javascript/submission_form/form.vue
+++ b/app/javascript/submission_form/form.vue
@@ -775,6 +775,7 @@ export default {
provide () {
return {
baseUrl: this.baseUrl,
+ fetchOptions: this.fetchOptions,
scrollIntoArea: this.scrollIntoArea,
scrollIntoField: this.scrollIntoField,
t: this.t
@@ -1002,6 +1003,11 @@ export default {
required: false,
default: false
},
+ viewToken: {
+ type: String,
+ required: false,
+ default: ''
+ },
attribution: {
type: Boolean,
required: false,
@@ -1531,15 +1537,10 @@ export default {
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
window.history.replaceState({}, document.title, newUrl)
- return fetch(this.baseUrl + '/api/submitter_email_clicks', {
+ return fetch(this.baseUrl + `/s/${this.submitterSlug}/click_email`, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- t,
- submitter_slug: this.submitterSlug
- })
+ headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers },
+ body: JSON.stringify({ t })
})
} else {
return Promise.resolve({})
@@ -1555,29 +1556,20 @@ export default {
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
window.history.replaceState({}, document.title, newUrl)
- return fetch(this.baseUrl + '/api/submitter_sms_clicks', {
+ return fetch(this.baseUrl + `/s/${this.submitterSlug}/click_sms`, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- c,
- submitter_slug: this.submitterSlug
- })
+ headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers },
+ body: JSON.stringify({ c })
})
} else {
return Promise.resolve({})
}
},
trackViewForm () {
- fetch(this.baseUrl + '/api/submitter_form_views', {
+ fetch(this.baseUrl + `/s/${this.submitterSlug}/view`, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- submitter_slug: this.submitterSlug
- })
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ v: this.viewToken })
})
},
previousSignatureValueFor (field) {
diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue
index 46e560c53..65cf82897 100644
--- a/app/javascript/submission_form/initials_step.vue
+++ b/app/javascript/submission_form/initials_step.vue
@@ -202,7 +202,7 @@ export default {
MarkdownContent,
IconArrowsDiagonalMinimize2
},
- inject: ['baseUrl', 't'],
+ inject: ['baseUrl', 'fetchOptions', 't'],
props: {
field: {
type: Object,
@@ -443,6 +443,7 @@ export default {
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
+ headers: { ...this.fetchOptions.headers },
body: formData
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue
index c22e463f6..e9eaafd4e 100644
--- a/app/javascript/submission_form/signature_step.vue
+++ b/app/javascript/submission_form/signature_step.vue
@@ -376,7 +376,7 @@ export default {
IconSignature,
IconArrowsDiagonalMinimize2
},
- inject: ['baseUrl', 't'],
+ inject: ['baseUrl', 'fetchOptions', 't'],
props: {
field: {
type: Object,
@@ -952,6 +952,7 @@ export default {
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
+ headers: { ...this.fetchOptions.headers },
body: formData
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
diff --git a/app/views/submit_form/_submission_form.html.erb b/app/views/submit_form/_submission_form.html.erb
index a590b33df..b7290509a 100644
--- a/app/views/submit_form/_submission_form.html.erb
+++ b/app/views/submit_form/_submission_form.html.erb
@@ -2,4 +2,4 @@
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
-
+
diff --git a/app/views/submit_form/email_2fa.html.erb b/app/views/submit_form/email_2fa.html.erb
index 856b0926f..b79479d2e 100644
--- a/app/views/submit_form/email_2fa.html.erb
+++ b/app/views/submit_form/email_2fa.html.erb
@@ -59,7 +59,7 @@
<% else %>
<% if params[:t] %>
- <%= button_to '', api_submitter_email_clicks_path, params: { submitter_slug: @submitter.slug, t: params[:t] }, method: :post, class: 'hidden' %>
+ <%= button_to '', submit_form_click_email_path(@submitter.slug), params: { t: params[:t] }, method: :post, class: 'hidden' %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 57f6023a5..a0c9869f9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -160,6 +160,9 @@ Rails.application.routes.draw do
resources :delegate, only: %i[create], controller: 'submit_form_delegate'
resources :invite, only: %i[create], controller: 'submit_form_invite'
resources :metadata, only: %i[index], controller: 'submit_form_metadata'
+ resource :click_email, only: %i[create], controller: 'submit_form_click_email'
+ resources :view, only: %i[create], controller: 'submit_form_view'
+ resources :upload, only: %i[create], controller: 'submit_form_upload'
resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development?
get :completed
get :delegated
diff --git a/lib/submitters/authorized_for_form.rb b/lib/submitters/authorized_for_form.rb
index 81048a162..4e7cacaa4 100644
--- a/lib/submitters/authorized_for_form.rb
+++ b/lib/submitters/authorized_for_form.rb
@@ -6,8 +6,24 @@ module Submitters
module_function
- def call(submitter, current_user, request)
- pass_email_2fa?(submitter, request) && pass_link_2fa?(submitter, current_user, request)
+ def call(submitter, current_user, request, with_order: false)
+ pass_email_2fa?(submitter, request) &&
+ pass_link_2fa?(submitter, current_user, request) &&
+ (!with_order || pass_submitters_order?(submitter))
+ end
+
+ def pass_submitters_order?(submitter)
+ return false unless submitter
+
+ submission = submitter.submission
+
+ return true if submission.submitters_order_random?
+ return true if (submission.template_submitters || submission.template.submitters).size < 2
+ return true if submission.template&.preferences&.dig('submitters_order') != 'preserved' &&
+ !AccountConfig.exists?(account_id: submitter.account_id,
+ key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true)
+
+ Submitters.current_submitter_order?(submitter)
end
def pass_email_2fa?(submitter, request)