- <% if submission.expired? %>
+ <% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
<%= t('expired') %>
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml
index 15ecb073..d807d374 100644
--- a/config/locales/i18n.yml
+++ b/config/locales/i18n.yml
@@ -22,6 +22,9 @@ en: &en
hi_there: Hi there
thanks: Thanks
private: Private
+ stripe_integration: Stripe Integration
+ stripe_account_has_been_connected: Stripe account has been connected.
+ re_connect_stripe: Re-connect Stripe
bcc_recipients: BCC recipients
resend_pending: Re-send pending
always_enforce_signing_order: Always enforce the signing order
@@ -46,7 +49,7 @@ en: &en
team_member_permissions: Team member permissions
entire_team: Entire team
admin_only: Admin only
- accessiable_by: Accessiable by
+ accessiable_by: Accessible by
team_access: Team access
document_download_filename_format: Document download filename format
document_name: Document Name
@@ -655,7 +658,7 @@ en: &en
completed_at: Completed at
edit_recipient: Edit Recipient
update_recipient: Update Recipient
- use_international_format_1xxx_: 'Use internatioanl format: +1xxx...'
+ use_international_format_1xxx_: 'Use international format: +1xxx...'
submitter_cannot_be_updated: Submitter cannot be updated.
at_least_one_field_must_be_filled: At least one field must be filled.
archived_users: Archived Users
@@ -741,11 +744,12 @@ en: &en
two_months: 2 months
three_months: 3 months
eu_data_residency: EU data residency
+ please_enter_your_email_address_associated_with_the_completed_submission: Please enter your email address associated with the completed submission.
submission_sources:
api: API
bulk: Bulk Send
embed: Embedding
- invie: Invite
+ invite: Invite
link: Link
submission_event_names:
send_email_to_html: 'Email sent to %{submitter_name}'
@@ -827,6 +831,9 @@ en: &en
read: Read your data
es: &es
+ stripe_integration: Integración con Stripe
+ stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
+ re_connect_stripe: Volver a conectar Stripe
private: Privado
resend_pending: Reenviar pendiente
ensure_unique_recipients: Asegurar destinatarios únicos
@@ -1549,6 +1556,7 @@ es: &es
two_months: 2 meses
three_months: 3 meses
eu_data_residency: Datos alojados UE
+ please_enter_your_email_address_associated_with_the_completed_submission: Por favor, introduce tu dirección de correo electrónico asociada con el envío completado.
submission_sources:
api: API
bulk: Envío masivo
@@ -1635,6 +1643,9 @@ es: &es
read: Leer tus datos
it: &it
+ stripe_integration: Integrazione Stripe
+ stripe_account_has_been_connected: L'account Stripe è stato collegato.
+ re_connect_stripe: Ricollega Stripe
private: Privato
resend_pending: Reinvia in sospeso
ensure_unique_recipients: Assicurarsi destinatari unici
@@ -2355,6 +2366,7 @@ it: &it
two_months: 2 mesi
three_months: 3 mesi
eu_data_residency: "Dati nell'UE"
+ please_enter_your_email_address_associated_with_the_completed_submission: "Inserisci il tuo indirizzo email associato all'invio completato."
submission_sources:
api: API
bulk: Invio massivo
@@ -2441,6 +2453,9 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
+ stripe_integration: Intégration Stripe
+ stripe_account_has_been_connected: Le compte Stripe a été connecté.
+ re_connect_stripe: Reconnecter Stripe
private: Privé
resend_pending: Renvoyer en attente
ensure_unique_recipients: Assurer l'unicité des destinataires
@@ -3164,6 +3179,7 @@ fr: &fr
two_months: 2 mois
three_months: 3 mois
eu_data_residency: "Données dans l'UE"
+ please_enter_your_email_address_associated_with_the_completed_submission: "Veuillez saisir l'adresse e-mail associée à l'envoi complété."
submission_sources:
api: API
bulk: Envoi en masse
@@ -3250,6 +3266,9 @@ fr: &fr
read: Lire vos données
pt: &pt
+ stripe_integration: Integração com Stripe
+ stripe_account_has_been_connected: Conta Stripe foi conectada.
+ re_connect_stripe: Reconectar Stripe
private: Privado
resend_pending: Re-enviar pendente
ensure_unique_recipients: Garantir destinatários únicos
@@ -3972,6 +3991,7 @@ pt: &pt
two_months: 2 meses
three_months: 3 meses
eu_data_residency: Dados na UE
+ please_enter_your_email_address_associated_with_the_completed_submission: Por favor, insira seu e-mail associado ao envio concluído.
submission_sources:
api: API
bulk: Envio em massa
@@ -4059,6 +4079,9 @@ pt: &pt
read: Ler seus dados
de: &de
+ stripe_integration: Stripe-Integration
+ stripe_account_has_been_connected: Stripe-Konto wurde verbunden.
+ re_connect_stripe: Stripe erneut verbinden
private: Privat
resend_pending: Ausstehende erneut senden
ensure_unique_recipients: Stellen Sie einzigartige Empfänger sicher
@@ -4164,9 +4187,9 @@ de: &de
sending: Senden
resubmit: Erneut einreichen
or: oder
- download_documents: Dokumente herunterladen
+ download_documents: Dokumente downloaden
downloading: Lädt herunter
- download: Laden
+ download: Download
decline: Ablehnen
declined: Abgelehnt
decline_reason: Ablehnungsgrund
@@ -4781,6 +4804,7 @@ de: &de
two_months: 2 Monate
three_months: 3 Monate
eu_data_residency: EU-Datenspeicher
+ please_enter_your_email_address_associated_with_the_completed_submission: Bitte gib deine E-Mail-Adresse ein, die mit der abgeschlossenen Übermittlung verknüpft ist.
submission_sources:
api: API
bulk: Massenversand
@@ -4933,6 +4957,7 @@ pl:
count_documents_signed_with_html: '%{count} dokumentów podpisanych za pomocą'
open_source_documents_software: 'oprogramowanie do dokumentów open source'
eu_data_residency: Dane w UE
+ please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem.
uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@@ -5001,6 +5026,7 @@ uk:
count_documents_signed_with_html: '%{count} документів підписано за допомогою'
open_source_documents_software: 'відкрите програмне забезпечення для документів'
eu_data_residency: 'Зберігання даних в ЄС'
+ please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@@ -5069,6 +5095,7 @@ cs:
count_documents_signed_with_html: '%{count} dokumentů podepsáno pomocí'
open_source_documents_software: 'open source software pro dokumenty'
eu_data_residency: 'Uložení dat v EU'
+ please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním.
he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@@ -5137,6 +5164,7 @@ he:
count_documents_signed_with_html: '%{count} מסמכים נחתמו באמצעות'
open_source_documents_software: 'תוכנה בקוד פתוח למסמכים'
eu_data_residency: 'נתונים באיחוד האירופי '
+ please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
@@ -5205,6 +5233,7 @@ nl:
count_documents_signed_with_html: '%{count} documenten ondertekend met'
open_source_documents_software: 'open source documenten software'
eu_data_residency: Gegevens EU
+ please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening.
ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@@ -5273,6 +5302,7 @@ ar:
count_documents_signed_with_html: '%{count} مستندات تم توقيعها باستخدام'
open_source_documents_software: 'برنامج مستندات مفتوح المصدر'
eu_data_residency: 'بيانات في الاتحاد الأوروبي'
+ please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@@ -5341,6 +5371,7 @@ ko:
count_documents_signed_with_html: '%{count}개의 문서가 다음을 통해 서명됨'
open_source_documents_software: '오픈소스 문서 소프트웨어'
eu_data_residency: 'EU 데이터 보관'
+ please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
@@ -5409,6 +5440,7 @@ ja:
count_documents_signed_with_html: '%{count} 件のドキュメントが以下で署名されました'
open_source_documents_software: 'オープンソースのドキュメントソフトウェア'
eu_data_residency: 'EU データ居住'
+ please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
en-US:
<<: *en
diff --git a/config/routes.rb b/config/routes.rb
index ba05e364..331ca82d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -57,7 +57,7 @@ Rails.application.routes.draw do
resources :verify_pdf_signature, only: %i[create]
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
- resources :account_configs, only: %i[create]
+ resources :account_configs, only: %i[create destroy]
resources :user_configs, only: %i[create]
resources :encrypted_user_configs, only: %i[destroy]
resources :timestamp_server, only: %i[create]
diff --git a/lib/load_bmp.rb b/lib/load_bmp.rb
new file mode 100644
index 00000000..2dc3269e
--- /dev/null
+++ b/lib/load_bmp.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+module LoadBmp
+ module_function
+
+ # rubocop:disable Metrics
+ def call(bmp_bytes)
+ bmp_bytes = bmp_bytes.b
+
+ header_data = parse_bmp_headers(bmp_bytes)
+
+ raw_pixel_data_from_file = extract_raw_pixel_data_blob(
+ bmp_bytes,
+ header_data[:pixel_data_offset],
+ header_data[:bmp_stride],
+ header_data[:height]
+ )
+
+ final_pixel_data = prepare_unpadded_pixel_data_string(
+ raw_pixel_data_from_file,
+ header_data[:bpp],
+ header_data[:width],
+ header_data[:height],
+ header_data[:bmp_stride]
+ )
+
+ bands = header_data[:bpp] / 8
+
+ unless header_data[:bpp] == 24 || header_data[:bpp] == 32
+ raise ArgumentError, "Conversion for #{header_data[:bpp]}-bpp BMP not implemented."
+ end
+
+ image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
+
+ image = image.flip(:vertical) if header_data[:orientation] == -1
+
+ image_rgb =
+ if bands == 3
+ image.recomb(band3_recomb)
+ elsif bands == 4
+ image.recomb(band4_recomb)
+ end
+
+ image_rgb = image_rgb.copy(interpretation: :srgb) if image_rgb.interpretation != :srgb
+
+ image_rgb
+ end
+
+ def parse_bmp_headers(bmp_bytes)
+ raise ArgumentError, 'BMP data too short for file header (14 bytes).' if bmp_bytes.bytesize < 14
+
+ signature, pixel_data_offset = bmp_bytes.unpack('a2@10L<')
+
+ raise ArgumentError, "Not a valid BMP file (invalid signature 'BM')." if signature != 'BM'
+
+ raise ArgumentError, 'BMP data too short for info header size field (4 bytes).' if bmp_bytes.bytesize < (14 + 4)
+
+ info_header_size = bmp_bytes.unpack1('@14L<')
+
+ min_expected_info_header_size = 40
+
+ if info_header_size < min_expected_info_header_size
+ raise ArgumentError,
+ "Unsupported BMP info header size: #{info_header_size}. Expected at least #{min_expected_info_header_size}."
+ end
+
+ header_and_info_header_min_bytes = 14 + min_expected_info_header_size
+
+ if bmp_bytes.bytesize < header_and_info_header_min_bytes
+ raise ArgumentError,
+ 'BMP data too short for essential BITMAPINFOHEADER fields ' \
+ "(requires #{header_and_info_header_min_bytes} bytes total)."
+ end
+
+ _header_size_check, width, raw_height_from_header, planes, bpp, compression =
+ bmp_bytes.unpack('@14L bmp_bytes.bytesize
+ actual_available = bmp_bytes.bytesize - pixel_data_offset
+ actual_available = 0 if actual_available.negative?
+ raise ArgumentError,
+ "Pixel data segment (offset #{pixel_data_offset}, expected size #{expected_pixel_data_size}) " \
+ "exceeds BMP file size (#{bmp_bytes.bytesize}). " \
+ "Only #{actual_available} bytes available after offset."
+ end
+
+ raw_pixel_data_from_file = bmp_bytes.byteslice(pixel_data_offset, expected_pixel_data_size)
+
+ if raw_pixel_data_from_file.nil? || raw_pixel_data_from_file.bytesize < expected_pixel_data_size
+ raise ArgumentError,
+ "Extracted pixel data is smaller (#{raw_pixel_data_from_file&.bytesize || 0} bytes) " \
+ "than expected (#{expected_pixel_data_size} bytes based on stride and height)."
+ end
+
+ raw_pixel_data_from_file
+ end
+
+ def prepare_unpadded_pixel_data_string(raw_pixel_data_from_file, bpp, width, height, bmp_stride)
+ bytes_per_pixel = bpp / 8
+ actual_row_width_bytes = width * bytes_per_pixel
+
+ unpadded_rows = Array.new(height)
+ current_offset_in_blob = 0
+
+ height.times do |i|
+ if current_offset_in_blob + actual_row_width_bytes > raw_pixel_data_from_file.bytesize
+ raise ArgumentError,
+ "Not enough data in pixel blob for row #{i}. Offset #{current_offset_in_blob}, " \
+ "row width #{actual_row_width_bytes}, blob size #{raw_pixel_data_from_file.bytesize}"
+ end
+
+ unpadded_row_slice = raw_pixel_data_from_file.byteslice(current_offset_in_blob, actual_row_width_bytes)
+
+ if unpadded_row_slice.nil? || unpadded_row_slice.bytesize < actual_row_width_bytes
+ raise ArgumentError, "Failed to slice a full unpadded row from pixel data blob for row #{i}."
+ end
+
+ unpadded_rows[i] = unpadded_row_slice
+ current_offset_in_blob += bmp_stride
+ end
+
+ unpadded_rows.join
+ end
+
+ def band3_recomb
+ @band3_recomb ||=
+ Vips::Image.new_from_array(
+ [
+ [0, 0, 1],
+ [0, 1, 0],
+ [1, 0, 0]
+ ]
+ )
+ end
+
+ def band4_recomb
+ @band4_recomb ||= Vips::Image.new_from_array(
+ [
+ [0, 0, 1, 0],
+ [0, 1, 0, 0],
+ [1, 0, 0, 0]
+ ]
+ )
+ end
+ # rubocop:enable Metrics
+end
diff --git a/lib/load_ico.rb b/lib/load_ico.rb
new file mode 100644
index 00000000..2c48d766
--- /dev/null
+++ b/lib/load_ico.rb
@@ -0,0 +1,209 @@
+# frozen_string_literal: true
+
+module LoadIco
+ BI_RGB = 0
+
+ module_function
+
+ # rubocop:disable Metrics
+ def call(ico_bytes)
+ io = StringIO.new(ico_bytes)
+ _reserved, type, count = io.read(6)&.unpack('S= 40
+
+ dib_params_bytes = dib_io.read(36)
+ return nil unless dib_params_bytes && dib_params_bytes.bytesize == 36
+
+ dib_width, dib_actual_height_field, dib_planes, dib_bpp,
+ dib_compression, _dib_image_size, _xpels, _ypels,
+ dib_clr_used, _dib_clr_important = dib_params_bytes.unpack('l= min_xor_bytes_needed
+
+ and_mask_bits_for_row = []
+ if has_and_mask
+ dib_io.seek(and_mask_data_offset + (y_dib_row * and_scanline_stride))
+ and_mask_scanline_bytes = dib_io.read(and_scanline_stride)
+ min_and_bytes_needed = ((dib_width * 1) + 7) / 8
+ return nil unless and_mask_scanline_bytes && and_mask_scanline_bytes.bytesize >= min_and_bytes_needed
+
+ (0...dib_width).each do |x_pixel|
+ byte_index = x_pixel / 8
+ bit_index_in_byte = 7 - (x_pixel % 8)
+ byte_val = and_mask_scanline_bytes.getbyte(byte_index)
+ and_mask_bits_for_row << ((byte_val >> bit_index_in_byte) & 1)
+ end
+ end
+
+ (0...dib_width).each do |x_pixel|
+ r = 0
+ g = 0
+ b = 0
+ a = 255
+
+ case dib_bpp
+ when 32
+ offset = x_pixel * 4
+ blue = xor_scanline_bytes.getbyte(offset)
+ green = xor_scanline_bytes.getbyte(offset + 1)
+ red = xor_scanline_bytes.getbyte(offset + 2)
+ alpha_val = xor_scanline_bytes.getbyte(offset + 3)
+ r = red
+ g = green
+ b = blue
+ a = alpha_val
+ when 24
+ offset = x_pixel * 3
+ blue = xor_scanline_bytes.getbyte(offset)
+ green = xor_scanline_bytes.getbyte(offset + 1)
+ red = xor_scanline_bytes.getbyte(offset + 2)
+ r = red
+ g = green
+ b = blue
+ when 8
+ idx = xor_scanline_bytes.getbyte(x_pixel)
+ r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
+ r = r_p
+ g = g_p
+ b = b_p
+ a = a_p
+ when 4
+ byte_val = xor_scanline_bytes.getbyte(x_pixel / 2)
+ idx = (x_pixel.even? ? (byte_val >> 4) : (byte_val & 0x0F))
+ r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
+ r = r_p
+ g = g_p
+ b = b_p
+ a = a_p
+ when 1
+ byte_val = xor_scanline_bytes.getbyte(x_pixel / 8)
+ idx = (byte_val >> (7 - (x_pixel % 8))) & 1
+ r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
+ r = r_p
+ g = g_p
+ b = b_p
+ a = a_p
+ end
+
+ if has_and_mask && !and_mask_bits_for_row.empty?
+ a = and_mask_bits_for_row[x_pixel] == 1 ? 0 : 255
+ end
+ flat_rgba_pixels.push(r, g, b, a)
+ end
+ end
+
+ pixel_data_string = flat_rgba_pixels.pack('C*')
+
+ expected_bytes = dib_width * image_pixel_height * 4
+
+ return nil unless pixel_data_string.bytesize == expected_bytes && expected_bytes.positive?
+
+ Vips::Image.new_from_memory(
+ pixel_data_string,
+ dib_width,
+ image_pixel_height,
+ 4,
+ :uchar
+ )
+ end
+ # rubocop:enable Metrics
+end
diff --git a/lib/pdfium.rb b/lib/pdfium.rb
new file mode 100644
index 00000000..554f7a6b
--- /dev/null
+++ b/lib/pdfium.rb
@@ -0,0 +1,412 @@
+# frozen_string_literal: true
+
+class Pdfium
+ extend FFI::Library
+
+ LIB_NAME = 'pdfium'
+
+ begin
+ ffi_lib case FFI::Platform::OS
+ when 'darwin'
+ [
+ "lib#{LIB_NAME}.dylib",
+ '/Applications/LibreOffice.app/Contents/Frameworks/libpdfiumlo.dylib'
+ ]
+ else
+ "lib#{LIB_NAME}.so"
+ end
+ rescue LoadError => e
+ raise "Could not load libpdfium library. Make sure it's installed and in your library path. Error: #{e.message}"
+ end
+
+ typedef :pointer, :FPDF_STRING
+ typedef :pointer, :FPDF_DOCUMENT
+ typedef :pointer, :FPDF_PAGE
+ typedef :pointer, :FPDF_BITMAP
+ typedef :pointer, :FPDF_FORMHANDLE
+
+ MAX_SIZE = 32_767
+
+ FPDF_ANNOT = 0x01
+ FPDF_LCD_TEXT = 0x02
+ FPDF_NO_NATIVETEXT = 0x04
+ FPDF_GRAYSCALE = 0x08
+ FPDF_REVERSE_BYTE_ORDER = 0x10
+ FPDF_RENDER_LIMITEDIMAGECACHE = 0x200
+ FPDF_RENDER_FORCEHALFTONE = 0x400
+ FPDF_PRINTING = 0x800
+
+ # rubocop:disable Naming/ClassAndModuleCamelCase
+ class FPDF_LIBRARY_CONFIG < FFI::Struct
+ layout :version, :int,
+ :m_pUserFontPaths, :pointer,
+ :m_pIsolate, :pointer,
+ :m_v8EmbedderSlot, :uint,
+ :m_pPlatform, :pointer,
+ :m_RendererType, :int
+ end
+ # rubocop:enable Naming/ClassAndModuleCamelCase
+
+ attach_function :FPDF_InitLibraryWithConfig, [:pointer], :void
+ attach_function :FPDF_DestroyLibrary, [], :void
+
+ attach_function :FPDF_LoadDocument, %i[string FPDF_STRING], :FPDF_DOCUMENT
+ attach_function :FPDF_LoadMemDocument, %i[pointer int FPDF_STRING], :FPDF_DOCUMENT
+ attach_function :FPDF_CloseDocument, [:FPDF_DOCUMENT], :void
+ attach_function :FPDF_GetPageCount, [:FPDF_DOCUMENT], :int
+ attach_function :FPDF_GetLastError, [], :ulong
+
+ attach_function :FPDF_LoadPage, %i[FPDF_DOCUMENT int], :FPDF_PAGE
+ attach_function :FPDF_ClosePage, [:FPDF_PAGE], :void
+ attach_function :FPDF_GetPageWidthF, [:FPDF_PAGE], :float
+ attach_function :FPDF_GetPageHeightF, [:FPDF_PAGE], :float
+
+ attach_function :FPDFBitmap_Create, %i[int int int], :FPDF_BITMAP
+ attach_function :FPDFBitmap_CreateEx, %i[int int int pointer int], :FPDF_BITMAP
+ attach_function :FPDFBitmap_Destroy, [:FPDF_BITMAP], :void
+ attach_function :FPDFBitmap_GetBuffer, [:FPDF_BITMAP], :pointer
+ attach_function :FPDFBitmap_GetWidth, [:FPDF_BITMAP], :int
+ attach_function :FPDFBitmap_GetHeight, [:FPDF_BITMAP], :int
+ attach_function :FPDFBitmap_GetStride, [:FPDF_BITMAP], :int
+ attach_function :FPDFBitmap_FillRect, %i[FPDF_BITMAP int int int int ulong], :void
+
+ attach_function :FPDF_RenderPageBitmap, %i[FPDF_BITMAP FPDF_PAGE int int int int int int], :void
+
+ typedef :int, :FPDF_BOOL
+ typedef :pointer, :IPDF_JSPLATFORM
+
+ # rubocop:disable Naming/ClassAndModuleCamelCase
+ class FPDF_FORMFILLINFO_V2 < FFI::Struct
+ layout :version, :int,
+ :Release, :pointer,
+ :FFI_Invalidate, :pointer,
+ :FFI_OutputSelectedRect, :pointer,
+ :FFI_SetCursor, :pointer,
+ :FFI_SetTimer, :pointer,
+ :FFI_KillTimer, :pointer,
+ :FFI_GetLocalTime, :pointer,
+ :FFI_OnChange, :pointer,
+ :FFI_GetPage, :pointer,
+ :FFI_GetCurrentPage, :pointer,
+ :FFI_GetRotation, :pointer,
+ :FFI_ExecuteNamedAction, :pointer,
+ :FFI_SetTextFieldFocus, :pointer,
+ :FFI_DoURIAction, :pointer,
+ :FFI_DoGoToAction, :pointer,
+ :m_pJsPlatform, :IPDF_JSPLATFORM,
+ :xfa_disabled, :FPDF_BOOL,
+ :FFI_DisplayCaret, :pointer,
+ :FFI_GetCurrentPageIndex, :pointer,
+ :FFI_SetCurrentPage, :pointer,
+ :FFI_GotoURL, :pointer,
+ :FFI_GetPageViewRect, :pointer,
+ :FFI_PageEvent, :pointer,
+ :FFI_PopupMenu, :pointer,
+ :FFI_OpenFile, :pointer,
+ :FFI_EmailTo, :pointer,
+ :FFI_UploadTo, :pointer,
+ :FFI_GetPlatform, :pointer,
+ :FFI_GetLanguage, :pointer,
+ :FFI_DownloadFromURL, :pointer,
+ :FFI_PostRequestURL, :pointer,
+ :FFI_PutRequestURL, :pointer,
+ :FFI_OnFocusChange, :pointer,
+ :FFI_DoURIActionWithKeyboardModifier, :pointer
+ end
+ # rubocop:enable Naming/ClassAndModuleCamelCase
+
+ attach_function :FPDFDOC_InitFormFillEnvironment, %i[FPDF_DOCUMENT pointer], :FPDF_FORMHANDLE
+ attach_function :FPDFDOC_ExitFormFillEnvironment, [:FPDF_FORMHANDLE], :void
+ attach_function :FPDF_FFLDraw, %i[FPDF_FORMHANDLE FPDF_BITMAP FPDF_PAGE int int int int int int], :void
+
+ FPDF_ERR_SUCCESS = 0
+ FPDF_ERR_UNKNOWN = 1
+ FPDF_ERR_FILE = 2
+ FPDF_ERR_FORMAT = 3
+ FPDF_ERR_PASSWORD = 4
+ FPDF_ERR_SECURITY = 5
+ FPDF_ERR_PAGE = 6
+
+ PDFIUM_ERRORS = {
+ FPDF_ERR_SUCCESS => 'Success',
+ FPDF_ERR_UNKNOWN => 'Unknown error',
+ FPDF_ERR_FILE => 'Error open file',
+ FPDF_ERR_FORMAT => 'Invalid format',
+ FPDF_ERR_PASSWORD => 'Incorrect password',
+ FPDF_ERR_SECURITY => 'Security scheme error',
+ FPDF_ERR_PAGE => 'Page not found'
+ }.freeze
+
+ class PdfiumError < StandardError; end
+
+ def self.error_message(code)
+ PDFIUM_ERRORS[code] || "Unknown error code: #{code}"
+ end
+
+ def self.check_last_error(context_message = 'PDFium operation failed')
+ error_code = FPDF_GetLastError()
+
+ return if error_code == FPDF_ERR_SUCCESS
+
+ raise PdfiumError, "#{context_message}: #{error_message(error_code)} (Code: #{error_code})"
+ end
+
+ class Document
+ attr_reader :document_ptr, :form_handle
+
+ def initialize(document_ptr, source_buffer = nil)
+ raise ArgumentError, 'document_ptr cannot be nil' if document_ptr.nil? || document_ptr.null?
+
+ @document_ptr = document_ptr
+
+ @pages = {}
+ @closed = false
+ @source_buffer = source_buffer
+ @form_handle = FFI::Pointer::NULL
+ @form_fill_info_mem = FFI::Pointer::NULL
+
+ init_form_fill_environment
+ end
+
+ def init_form_fill_environment
+ return if @document_ptr.null?
+
+ @form_fill_info_mem = FFI::MemoryPointer.new(FPDF_FORMFILLINFO_V2.size)
+
+ form_fill_info_struct = FPDF_FORMFILLINFO_V2.new(@form_fill_info_mem)
+ form_fill_info_struct[:version] = 2
+
+ @form_handle = Pdfium.FPDFDOC_InitFormFillEnvironment(@document_ptr, @form_fill_info_mem)
+ end
+
+ def page_count
+ @page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr)
+ end
+
+ def self.open_file(file_path, password = nil)
+ doc_ptr = Pdfium.FPDF_LoadDocument(file_path, password)
+
+ if doc_ptr.null?
+ Pdfium.check_last_error("Failed to load document from file '#{file_path}'")
+
+ raise PdfiumError, "Failed to load document from file '#{file_path}', pointer is NULL."
+ end
+
+ doc = new(doc_ptr)
+
+ return doc unless block_given?
+
+ begin
+ yield doc
+ ensure
+ doc.close
+ end
+ end
+
+ def self.open_bytes(bytes, password = nil)
+ buffer = FFI::MemoryPointer.new(:char, bytes.bytesize)
+ buffer.put_bytes(0, bytes)
+
+ doc_ptr = Pdfium.FPDF_LoadMemDocument(buffer, bytes.bytesize, password)
+
+ if doc_ptr.null?
+ Pdfium.check_last_error('Failed to load document from memory')
+
+ raise PdfiumError, 'Failed to load document from memory, pointer is NULL.'
+ end
+
+ doc = new(doc_ptr, buffer)
+
+ return doc unless block_given?
+
+ begin
+ yield doc
+ ensure
+ doc.close
+ end
+ end
+
+ def closed?
+ @closed
+ end
+
+ def ensure_not_closed!
+ raise PdfiumError, 'Document is closed.' if closed?
+ end
+
+ def get_page(page_index)
+ ensure_not_closed!
+
+ unless page_index.is_a?(Integer) && page_index >= 0 && page_index < page_count
+ raise PdfiumError, "Page index #{page_index} out of range (0..#{page_count - 1})"
+ end
+
+ @pages[page_index] ||= Page.new(self, page_index)
+ end
+
+ def close
+ return if closed?
+
+ @pages.each_value { |page| page.close unless page.closed? }
+ @pages.clear
+
+ unless @form_handle.null?
+ Pdfium.FPDFDOC_ExitFormFillEnvironment(@form_handle)
+
+ @form_handle = FFI::Pointer::NULL
+ end
+
+ if @form_fill_info_mem && !@form_fill_info_mem.null?
+ @form_fill_info_mem.free
+ @form_fill_info_mem = FFI::Pointer::NULL
+ end
+
+ Pdfium.FPDF_CloseDocument(@document_ptr) unless @document_ptr.null?
+
+ @document_ptr = FFI::Pointer::NULL
+ @source_buffer = nil
+
+ @closed = true
+ end
+ end
+
+ class Page
+ attr_reader :document, :page_index, :page_ptr
+
+ def initialize(document, page_index)
+ raise ArgumentError, 'Document object is required' unless document.is_a?(Pdfium::Document)
+
+ @document = document
+ @document.ensure_not_closed!
+
+ @page_index = page_index
+
+ @page_ptr = Pdfium.FPDF_LoadPage(document.document_ptr, page_index)
+
+ if @page_ptr.null?
+ Pdfium.check_last_error("Failed to load page #{page_index}")
+
+ raise PdfiumError, "Failed to load page #{page_index}, pointer is NULL."
+ end
+
+ @closed = false
+ end
+
+ def width
+ @width ||= Pdfium.FPDF_GetPageWidthF(@page_ptr)
+ end
+
+ def height
+ @height ||= Pdfium.FPDF_GetPageHeightF(@page_ptr)
+ end
+
+ def closed?
+ @closed
+ end
+
+ def form_handle
+ @document.form_handle
+ end
+
+ def ensure_not_closed!
+ raise PdfiumError, 'Page is closed.' if closed?
+
+ @document.ensure_not_closed!
+ end
+
+ def render_to_bitmap(width: nil, height: nil, scale: nil, background_color: 0xFFFFFFFF,
+ flags: FPDF_ANNOT | FPDF_LCD_TEXT | FPDF_NO_NATIVETEXT | FPDF_REVERSE_BYTE_ORDER)
+ ensure_not_closed!
+
+ render_width, render_height = calculate_render_dimensions(width, height, scale)
+
+ bitmap_ptr = Pdfium.FPDFBitmap_Create(render_width, render_height, 1)
+
+ if bitmap_ptr.null?
+ Pdfium.check_last_error('Failed to create bitmap (potential pre-existing error)')
+
+ raise PdfiumError, 'Failed to create bitmap (FPDFBitmap_Create returned NULL)'
+ end
+
+ Pdfium.FPDFBitmap_FillRect(bitmap_ptr, 0, 0, render_width, render_height, background_color)
+
+ Pdfium.FPDF_RenderPageBitmap(bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags)
+
+ Pdfium.check_last_error('Failed to render page to bitmap')
+
+ unless form_handle.null?
+ Pdfium.FPDF_FFLDraw(form_handle, bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags)
+
+ Pdfium.check_last_error('Call to FPDF_FFLDraw completed (check for rendering issues if any)')
+ end
+
+ buffer_ptr = Pdfium.FPDFBitmap_GetBuffer(bitmap_ptr)
+ stride = Pdfium.FPDFBitmap_GetStride(bitmap_ptr)
+
+ bitmap_data = buffer_ptr.read_bytes(stride * render_height)
+
+ [bitmap_data, render_width, render_height]
+ ensure
+ Pdfium.FPDFBitmap_Destroy(bitmap_ptr) if bitmap_ptr && !bitmap_ptr.null?
+ end
+
+ def close
+ return if closed?
+
+ Pdfium.FPDF_ClosePage(@page_ptr) unless @page_ptr.null?
+
+ @page_ptr = FFI::Pointer::NULL
+
+ @closed = true
+ end
+
+ private
+
+ def calculate_render_dimensions(width_param, height_param, scale_param)
+ if scale_param
+ render_width = (width * scale_param).round
+ render_height = (height * scale_param).round
+ elsif width_param || height_param
+ if width_param && height_param
+ render_width = width_param
+ render_height = height_param
+ elsif width_param
+ scale_factor = width_param.to_f / width
+ render_width = width_param
+ render_height = (height * scale_factor).round
+ else
+ scale_factor = height_param.to_f / height
+ render_width = (width * scale_factor).round
+ render_height = height_param
+ end
+ else
+ render_width = width.to_i
+ render_height = height.to_i
+ end
+
+ [render_width.clamp(1, MAX_SIZE), render_height.clamp(1, MAX_SIZE)]
+ end
+ end
+
+ def self.initialize_library
+ config_mem = FFI::MemoryPointer.new(FPDF_LIBRARY_CONFIG.size)
+
+ config_struct = FPDF_LIBRARY_CONFIG.new(config_mem)
+ config_struct[:version] = 2
+ config_struct[:m_pUserFontPaths] = FFI::Pointer::NULL
+ config_struct[:m_pIsolate] = FFI::Pointer::NULL
+ config_struct[:m_v8EmbedderSlot] = 0
+
+ FPDF_InitLibraryWithConfig(config_mem)
+ end
+
+ def self.cleanup_library
+ FPDF_DestroyLibrary()
+ end
+
+ initialize_library
+
+ at_exit do
+ cleanup_library
+ end
+end
diff --git a/lib/submissions.rb b/lib/submissions.rb
index 90ebfad4..92e715ed 100644
--- a/lib/submissions.rb
+++ b/lib/submissions.rb
@@ -120,6 +120,8 @@ module Submissions
email = email.to_s.tr('/', ',')
+ return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
+
return email.downcase if email.include?(',') ||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
email.exclude?('.')
diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb
index ecfe9b84..32622049 100644
--- a/lib/submissions/generate_audit_trail.rb
+++ b/lib/submissions/generate_audit_trail.rb
@@ -309,7 +309,7 @@ module Submissions
image =
begin
- Vips::Image.new_from_buffer(attachment.download, '').autorot
+ Submissions::GenerateResultAttachments.load_vips_image(attachment).autorot
rescue Vips::Error
next unless attachment.content_type.starts_with?('image/')
next if attachment.byte_size.zero?
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index 0a5da813..e260192d 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -11,6 +11,9 @@ module Submissions
'Helvetica'
end
+ ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z}
+ BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
+
FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
FONT_BOLD_PATH
else
@@ -250,9 +253,7 @@ module Submissions
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
attachment = submitter.attachments.find { |a| a.uuid == value }
- attachments_data_cache[attachment.uuid] ||= attachment.download
-
- image = Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
+ image = load_vips_image(attachment, attachments_data_cache).autorot
reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
@@ -360,11 +361,9 @@ module Submissions
when 'image', 'signature', 'initials', 'stamp'
attachment = submitter.attachments.find { |a| a.uuid == value }
- attachments_data_cache[attachment.uuid] ||= attachment.download
-
image =
begin
- Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
+ load_vips_image(attachment, attachments_data_cache).autorot
rescue Vips::Error
next unless attachment.content_type.starts_with?('image/')
next if attachment.byte_size.zero?
@@ -736,14 +735,16 @@ module Submissions
page = pdf.pages.add
- scale = [A4_SIZE.first / attachment.metadata['width'].to_f,
- A4_SIZE.last / attachment.metadata['height'].to_f].min
+ image = attachment.preview_images.first
- page.box.width = attachment.metadata['width'] * scale
- page.box.height = attachment.metadata['height'] * scale
+ scale = [A4_SIZE.first / image.metadata['width'].to_f,
+ A4_SIZE.last / image.metadata['height'].to_f].min
+
+ page.box.width = image.metadata['width'] * scale
+ page.box.height = image.metadata['height'] * scale
page.canvas.image(
- StringIO.new(attachment.preview_images.first.download),
+ StringIO.new(image.download),
at: [0, 0],
width: page.box.width,
height: page.box.height
@@ -804,6 +805,20 @@ module Submissions
[]
end
+ def load_vips_image(attachment, cache = {})
+ cache[attachment.uuid] ||= attachment.download
+
+ data = cache[attachment.uuid]
+
+ if ICO_REGEXP.match?(attachment.content_type)
+ LoadIco.call(data)
+ elsif BMP_REGEXP.match?(attachment.content_type)
+ LoadBmp.call(data)
+ else
+ Vips::Image.new_from_buffer(data, '')
+ end
+ end
+
def h
Rails.application.routes.url_helpers
end
diff --git a/lib/templates/process_document.rb b/lib/templates/process_document.rb
index 54174b06..8ee1dff5 100644
--- a/lib/templates/process_document.rb
+++ b/lib/templates/process_document.rb
@@ -6,6 +6,7 @@ module Templates
FORMAT = '.png'
ATTACHMENT_NAME = 'preview_images'
+ BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
PDF_CONTENT_TYPE = 'application/pdf'
CONCURRENCY = 2
Q = 95
@@ -38,7 +39,13 @@ module Templates
def generate_preview_image(attachment, data)
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
- image = Vips::Image.new_from_buffer(data, '')
+ image =
+ if BMP_REGEXP.match?(attachment.content_type)
+ LoadBmp.call(data)
+ else
+ Vips::Image.new_from_buffer(data, '')
+ end
+
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size
@@ -77,11 +84,13 @@ module Templates
end
def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY)
+ doc = Pdfium::Document.open_bytes(data)
+
pool = Concurrent::FixedThreadPool.new(concurrency)
promises =
range.map do |page_number|
- Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) }
+ Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(doc, page_number) }
end
Concurrent::Promise.zip(*promises).value!.each do |blob|
@@ -95,31 +104,42 @@ module Templates
)
end
end
-
- pool.kill
+ ensure
+ doc&.close
+ pool&.kill
end
- def build_and_upload_blob(data, page_number)
- page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
- page = page.resize(MAX_WIDTH / page.width.to_f)
+ def build_and_upload_blob(doc, page_number, format = FORMAT)
+ doc_page = doc.get_page(page_number)
+
+ data, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
+
+ page = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
+
+ page = page.copy(interpretation: :srgb)
bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
- io = StringIO.new(page.write_to_buffer(FORMAT, compression: 7, filter: 0, bitdepth:,
- palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0))
+ data =
+ if format == FORMAT
+ page.write_to_buffer(format, compression: 7, filter: 0, bitdepth:,
+ palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0)
+ else
+ page.write_to_buffer(format, interlace: true, Q: JPEG_Q)
+ end
blob = ActiveStorage::Blob.new(
- filename: "#{page_number}#{FORMAT}",
+ filename: "#{page_number}#{format}",
metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
)
- blob.upload(io)
+ blob.upload(StringIO.new(data))
blob
- rescue Vips::Error => e
+ rescue Vips::Error, Pdfium::PdfiumError => e
Rollbar.warning(e) if defined?(Rollbar)
-
- nil
+ ensure
+ doc_page&.close
end
def maybe_flatten_form(data, pdf)
@@ -162,35 +182,19 @@ module Templates
end
def generate_pdf_preview_from_file(attachment, file_path, page_number)
- io = StringIO.new
-
- command = [
- 'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{JPEG_Q},optimize=y",
- '-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1',
- '-r', DPI, '-f', page_number + 1, '-l', page_number + 1,
- '-singlefile', Shellwords.escape(file_path), '-'
- ].join(' ')
-
- Open3.popen3(command) do |_, stdout, _, _|
- io.write(stdout.read)
-
- io.rewind
- end
-
- page = Vips::Image.new_from_buffer(io.read, '')
+ doc = Pdfium::Document.open_file(file_path)
- io.rewind
+ blob = build_and_upload_blob(doc, page_number, '.jpeg')
ApplicationRecord.no_touching do
ActiveStorage::Attachment.create!(
- blob: ActiveStorage::Blob.create_and_upload!(
- io:, filename: "#{page_number}.jpg",
- metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
- ),
+ blob: blob,
name: ATTACHMENT_NAME,
record: attachment
)
end
+ ensure
+ doc&.close
end
end
end
diff --git a/spec/factories/template_accesses.rb b/spec/factories/template_accesses.rb
new file mode 100644
index 00000000..9c33173e
--- /dev/null
+++ b/spec/factories/template_accesses.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :template_access do
+ template
+ user
+ end
+end
diff --git a/spec/factories/templates.rb b/spec/factories/templates.rb
index 8f619353..2091821c 100644
--- a/spec/factories/templates.rb
+++ b/spec/factories/templates.rb
@@ -14,6 +14,7 @@ FactoryBot.define do
%w[text date checkbox radio signature number multiple select initials image file stamp cells phone payment]
end
except_field_types { [] }
+ private_access_user { nil }
end
after(:create) do |template, ev|
@@ -343,5 +344,17 @@ FactoryBot.define do
template.save!
end
+
+ trait :with_admin_only_access do
+ after(:create) do |template|
+ create(:template_access, template:, user_id: TemplateAccess::ADMIN_USER_ID)
+ end
+ end
+
+ trait :with_private_access do
+ after(:create) do |template, ev|
+ create(:template_access, template:, user: ev.private_access_user || template.author)
+ end
+ end
end
end
diff --git a/spec/jobs/process_submitter_completion_job_spec.rb b/spec/jobs/process_submitter_completion_job_spec.rb
index 632003b3..cb357efc 100644
--- a/spec/jobs/process_submitter_completion_job_spec.rb
+++ b/spec/jobs/process_submitter_completion_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe ProcessSubmitterCompletionJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 b57e74b2..cf09d1e1 100644
--- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendFormCompletedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 c53a1999..1dcfb181 100644
--- a/spec/jobs/send_form_declined_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendFormDeclinedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 929a2187..77cd5bdd 100644
--- a/spec/jobs/send_form_started_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendFormStartedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 c3c7c216..c182af0d 100644
--- a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendFormViewedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
diff --git a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
index 05947f33..233f1661 100644
--- a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
+++ b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendSubmissionArchivedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 e2f0d26e..3a7053df 100644
--- a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb
+++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendSubmissionCompletedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 1b2e02fe..1decf1b0 100644
--- a/spec/jobs/send_submission_created_webhook_request_job_spec.rb
+++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendSubmissionCreatedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 14df0b3e..bb2b51c6 100644
--- a/spec/jobs/send_template_created_webhook_request_job_spec.rb
+++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendTemplateCreatedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
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 4451b071..2edcd280 100644
--- a/spec/jobs/send_template_updated_webhook_request_job_spec.rb
+++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe SendTemplateUpdatedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb
index 5a00e188..7021a6b3 100644
--- a/spec/lib/params/base_validator_spec.rb
+++ b/spec/lib/params/base_validator_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe Params::BaseValidator do
let(:validator) { described_class.new({}) }
diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb
index 020bd908..5f9e98db 100644
--- a/spec/requests/submissions_spec.rb
+++ b/spec/requests/submissions_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'rails_helper'
-
-describe 'Submission API', type: :request do
+describe 'Submission API' do
let(:account) { create(:account, :with_testing_account) }
let(:testing_account) { account.testing_accounts.first }
let(:author) { create(:user, account:) }
diff --git a/spec/requests/submitters_spec.rb b/spec/requests/submitters_spec.rb
index 023de7ee..faa7dd4d 100644
--- a/spec/requests/submitters_spec.rb
+++ b/spec/requests/submitters_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'rails_helper'
-
-describe 'Submitter API', type: :request do
+describe 'Submitter API' do
let(:account) { create(:account, :with_testing_account) }
let(:testing_account) { account.testing_accounts.first }
let(:author) { create(:user, account:) }
diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb
index 3d6e1284..fa2a362a 100644
--- a/spec/requests/templates_spec.rb
+++ b/spec/requests/templates_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'rails_helper'
-
-describe 'Templates API', type: :request do
+describe 'Templates API' do
let(:account) { create(:account, :with_testing_account) }
let(:testing_account) { account.testing_accounts.first }
let(:author) { create(:user, account:) }
diff --git a/spec/requests/tools_spec.rb b/spec/requests/tools_spec.rb
index b51f771c..3de12fcc 100644
--- a/spec/requests/tools_spec.rb
+++ b/spec/requests/tools_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'rails_helper'
-
-describe 'Tools API', type: :request do
+describe 'Tools API' do
let(:account) { create(:account) }
let(:author) { create(:user, account:) }
let(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') }
diff --git a/spec/system/account_settings_spec.rb b/spec/system/account_settings_spec.rb
index ae3bef28..7c194219 100644
--- a/spec/system/account_settings_spec.rb
+++ b/spec/system/account_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Account Settings' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/api_settings_spec.rb b/spec/system/api_settings_spec.rb
index 648e202d..a14ef36d 100644
--- a/spec/system/api_settings_spec.rb
+++ b/spec/system/api_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'API Settings' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/dashboard_spec.rb b/spec/system/dashboard_spec.rb
index f2bd11b1..81d16458 100644
--- a/spec/system/dashboard_spec.rb
+++ b/spec/system/dashboard_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Dashboard Page' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/email_settings_spec.rb b/spec/system/email_settings_spec.rb
index 5e2a00e5..f7d683b8 100644
--- a/spec/system/email_settings_spec.rb
+++ b/spec/system/email_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Email Settings' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/esign_spec.rb b/spec/system/esign_spec.rb
index b1a48f62..d7441d7e 100644
--- a/spec/system/esign_spec.rb
+++ b/spec/system/esign_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'PDF Signature Settings' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/newsletters_spec.rb b/spec/system/newsletters_spec.rb
index a4fc3c18..274ba7e3 100644
--- a/spec/system/newsletters_spec.rb
+++ b/spec/system/newsletters_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Newsletter' do
let(:user) { create(:user, account: create(:account)) }
diff --git a/spec/system/notifications_settings_spec.rb b/spec/system/notifications_settings_spec.rb
index 4f0d0f7c..2edd3eb5 100644
--- a/spec/system/notifications_settings_spec.rb
+++ b/spec/system/notifications_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Notifications Settings' do
let(:user) { create(:user, account: create(:account)) }
diff --git a/spec/system/personalization_settings_spec.rb b/spec/system/personalization_settings_spec.rb
index 9d16d0c0..8b7fa214 100644
--- a/spec/system/personalization_settings_spec.rb
+++ b/spec/system/personalization_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Personalization Settings', :js do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/personalization_spec.rb b/spec/system/personalization_spec.rb
index f82173a1..18aa04b1 100644
--- a/spec/system/personalization_spec.rb
+++ b/spec/system/personalization_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Personalization' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/profile_settings_spec.rb b/spec/system/profile_settings_spec.rb
index 351b636c..7b977d2d 100644
--- a/spec/system/profile_settings_spec.rb
+++ b/spec/system/profile_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Profile Settings' do
let(:user) { create(:user, account: create(:account)) }
diff --git a/spec/system/setup_spec.rb b/spec/system/setup_spec.rb
index 25c6df27..5235c9b5 100644
--- a/spec/system/setup_spec.rb
+++ b/spec/system/setup_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'App Setup' do
let(:form_data) do
{
diff --git a/spec/system/sign_in_spec.rb b/spec/system/sign_in_spec.rb
index e9e2f4d1..df0e4f02 100644
--- a/spec/system/sign_in_spec.rb
+++ b/spec/system/sign_in_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'rails_helper'
-
-RSpec.describe 'Sign In', type: :system do
+RSpec.describe 'Sign In' do
let(:account) { create(:account) }
let!(:user) { create(:user, account:, email: 'john.dou@example.com', password: 'strong_password') }
diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb
index 300edc2e..8bfa3e5e 100644
--- a/spec/system/signing_form_spec.rb
+++ b/spec/system/signing_form_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'rails_helper'
-
-RSpec.describe 'Signing Form', type: :system do
+RSpec.describe 'Signing Form' do
let(:account) { create(:account) }
let(:author) { create(:user, account:) }
@@ -13,7 +11,7 @@ RSpec.describe 'Signing Form', type: :system do
visit start_form_path(slug: template.slug)
end
- it 'shows the email step', type: :system do
+ it 'shows the email step' do
expect(page).to have_content('You have been invited to submit a form')
expect(page).to have_content("Invited by #{account.name}")
expect(page).to have_field('Email', type: 'email')
diff --git a/spec/system/storage_settings_spec.rb b/spec/system/storage_settings_spec.rb
index d57564d7..c11d0ea0 100644
--- a/spec/system/storage_settings_spec.rb
+++ b/spec/system/storage_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Storage Settings' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/submission_preview_spec.rb b/spec/system/submission_preview_spec.rb
index b231fcff..3048e96d 100644
--- a/spec/system/submission_preview_spec.rb
+++ b/spec/system/submission_preview_spec.rb
@@ -1,25 +1,59 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Submission Preview' do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
let(:template) { create(:template, account:, author: user) }
context 'when not submitted' do
- let(:submission) { create(:submission, template:, created_by_user: user) }
+ let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) }
- before do
- template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) }
+ context 'when user is signed in' do
+ before do
+ sign_in(user)
- sign_in(user)
+ visit submissions_preview_path(slug: submission.slug)
+ end
- visit submissions_preview_path(slug: submission.slug)
+ it 'completes the form' do
+ expect(page).to have_content('Not completed')
+ end
end
- it 'completes the form' do
- expect(page).to have_content('Not completed')
+ context 'when user is not signed in' do
+ context 'when submission is not completed' do
+ before do
+ create(:encrypted_config, account:, key: EncryptedConfig::EMAIL_SMTP_KEY, value: '{}')
+
+ submission.submitters.each { |s| s.update(completed_at: 1.day.ago) }
+
+ visit submissions_preview_path(slug: submission.slug)
+ end
+
+ it "sends a copy to the submitter's email" do
+ fill_in 'Email', with: submission.submitters.first.email
+ click_button 'Send copy to Email'
+
+ expect(page).to have_content('Email has been sent.')
+ end
+
+ it 'shows an error for an email not associated with the submission' do
+ fill_in 'Email', with: 'john.due@example.com'
+ click_button 'Send copy to Email'
+
+ expect(page).to have_content('Please enter your email address associated with the completed submission.')
+ end
+ end
+
+ it "doesn't display the email form if SMTP is not configured" do
+ submission.submitters.each { |s| s.update(completed_at: 1.day.ago) }
+
+ visit submissions_preview_path(slug: submission.slug)
+
+ expect(page).to have_content(template.name)
+ expect(page).not_to have_field('Email')
+ expect(page).not_to have_content('Send copy to Email')
+ end
end
end
end
diff --git a/spec/system/team_settings_spec.rb b/spec/system/team_settings_spec.rb
index 077c5104..0286267f 100644
--- a/spec/system/team_settings_spec.rb
+++ b/spec/system/team_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Team Settings' do
let(:account) { create(:account) }
let(:second_account) { create(:account) }
diff --git a/spec/system/template_builder_spec.rb b/spec/system/template_builder_spec.rb
index d14ce3b9..2f4b9456 100644
--- a/spec/system/template_builder_spec.rb
+++ b/spec/system/template_builder_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Template Builder' do
let(:account) { create(:account) }
let(:author) { create(:user, account:) }
diff --git a/spec/system/template_spec.rb b/spec/system/template_spec.rb
index 0b512041..a0ffe4e5 100644
--- a/spec/system/template_spec.rb
+++ b/spec/system/template_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Template' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
diff --git a/spec/system/webhook_settings_spec.rb b/spec/system/webhook_settings_spec.rb
index fbbcaea2..5b4cbaf2 100644
--- a/spec/system/webhook_settings_spec.rb
+++ b/spec/system/webhook_settings_spec.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rails_helper'
-
RSpec.describe 'Webhook Settings' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }