diff --git a/Gemfile b/Gemfile
index 661419ff..16d1261f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -109,4 +109,7 @@ end
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'hirb'
+# signing verification
+gem 'pdf-reader'
+
gem "rails-controller-testing", "~> 1.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index b98bce40..577ecff1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,6 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
+ Ascii85 (1.1.0)
actioncable (6.1.7.3)
actionpack (= 6.1.7.3)
activesupport (= 6.1.7.3)
@@ -62,6 +63,7 @@ GEM
zeitwerk (~> 2.3)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
+ afm (0.2.2)
aws-eventstream (1.2.0)
aws-partitions (1.734.0)
aws-record (2.10.1)
@@ -164,6 +166,7 @@ GEM
activerecord (>= 4.0.0)
globalid (1.1.0)
activesupport (>= 5.0)
+ hashery (2.1.2)
hashie (5.0.0)
hirb (0.7.3)
htmlentities (4.3.4)
@@ -270,6 +273,12 @@ GEM
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
+ pdf-reader (2.11.0)
+ Ascii85 (~> 1.0)
+ afm (~> 0.2.1)
+ hashery (~> 2.0)
+ ruby-rc4
+ ttfunk
pg (1.4.6)
pg_search (2.3.6)
activerecord (>= 5.2)
@@ -366,6 +375,7 @@ GEM
rspec-support (3.12.0)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
+ ruby-rc4 (0.1.5)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sass-rails (6.0.0)
@@ -418,6 +428,7 @@ GEM
thor (1.2.1)
tilt (2.1.0)
timeout (0.3.2)
+ ttfunk (1.7.0)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
@@ -486,6 +497,7 @@ DEPENDENCIES
omniauth
omniauth-google-oauth2
omniauth-rails_csrf_protection
+ pdf-reader
pg
pg_search
premailer-rails
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index fba004e1..2cf362c0 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -14,6 +14,7 @@
*= require static
*= require navody-frontend-fixes
*= require or-sr
+ *= require spinner
*= require_self
*/
diff --git a/app/assets/stylesheets/or-sr.scss b/app/assets/stylesheets/or-sr.scss
index c78c17fa..4c458190 100644
--- a/app/assets/stylesheets/or-sr.scss
+++ b/app/assets/stylesheets/or-sr.scss
@@ -83,28 +83,6 @@ div#or-sr-identifiers, div#acts-submission {
min-height: 40px;
}
- @keyframes spinner {
- to {transform: rotate(360deg);}
- }
-
- .spinner {
- position: absolute;
- top: 25%;
- left: 50%;
- }
-
- .spinner:before {
- content: '';
- box-sizing: border-box;
- position: absolute;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- border: 2px solid #ccc;
- border-top-color: #000;
- animation: spinner .6s linear infinite;
- }
-
#identifiers-sr input, #identifiers-foreign input {
width: 100%;
diff --git a/app/assets/stylesheets/spinner.scss b/app/assets/stylesheets/spinner.scss
new file mode 100644
index 00000000..a3b1ab68
--- /dev/null
+++ b/app/assets/stylesheets/spinner.scss
@@ -0,0 +1,26 @@
+@keyframes spinner {
+ to {transform: rotate(360deg);}
+}
+
+.spinner {
+ position: absolute;
+ top: 25%;
+ left: 50%;
+}
+
+.spinner:before {
+ content: '';
+ box-sizing: border-box;
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ border: 2px solid #ccc;
+ border-top-color: #000;
+ animation: spinner .6s linear infinite;
+}
+
+.spinner.spinner-lg:before {
+ width: 60px;
+ height: 60px;
+}
diff --git a/app/controllers/apps/acts_or_sr_app/acts_submissions_controller.rb b/app/controllers/apps/acts_or_sr_app/acts_submissions_controller.rb
index 9f0afaf2..332170e7 100644
--- a/app/controllers/apps/acts_or_sr_app/acts_submissions_controller.rb
+++ b/app/controllers/apps/acts_or_sr_app/acts_submissions_controller.rb
@@ -85,8 +85,7 @@ def form_check
# generates XML to be submitted
def create
- xml_form = UpvsSubmissions::FormBuilders::ApplicationForDocumentCopyFormBuilder.build_form(@application_form)
- render xml: xml_form.to_xml
+ render json: { id: UpvsSubmissions::Forms::ApplicationForDocumentCopy.create_form_attachment(@application_form) }.to_json
end
def callback
diff --git a/app/controllers/apps/general_agenda_app/general_agenda_controller.rb b/app/controllers/apps/general_agenda_app/general_agenda_controller.rb
new file mode 100644
index 00000000..18d95cb5
--- /dev/null
+++ b/app/controllers/apps/general_agenda_app/general_agenda_controller.rb
@@ -0,0 +1,55 @@
+module Apps
+ module GeneralAgendaApp
+ class GeneralAgendaController < ApplicationController
+ skip_forgery_protection only: [:index]
+
+ def index
+ # Configuration of the form - as API
+ attributes = {
+ title: params[:title].presence || 'Všeobecná agenda',
+ description: params[:description].presence || 'Formulár pre odoslanie podania pre všeobecnú agendu',
+ subject: params[:subject_placeholder],
+ text: params[:text_placeholder],
+ signed_required: params[:signed_required],
+ attachments_template: params[:attachments_template],
+ }.with_indifferent_access
+
+ # Appending whatever user submitted into recipient, subject, text and attachments
+ # + remembers the form configuration between submits
+ attributes.merge!(general_agenda_params || {})
+
+ @application_form = Apps::GeneralAgendaApp::GeneralAgenda::ApplicationForm.new(attributes)
+
+ return render :invalid_template unless @application_form.valid_template?
+
+ redirect_to_upvs_submission if @application_form.is_submitted && @application_form.valid?
+ end
+
+ def callback
+ end
+
+ private
+
+ def redirect_to_upvs_submission
+ @submission_form = UpvsSubmissions::Forms::GeneralAgenda.new(@application_form)
+
+ render :redirect_to_upvs_submission
+ end
+
+ def general_agenda_params
+ params[:apps_general_agenda_app_general_agenda_application_form]&.permit(
+ :title,
+ :description,
+ :subject,
+ :text,
+ :signed_required,
+ :recipient_name,
+ :recipient_uri,
+ :is_submitted,
+ attachments: {},
+ attachments_template: [:name, :description, :required, :signed_required]
+ )
+ end
+ end
+ end
+end
diff --git a/app/controllers/datahub/upvs/public_authority_edesks_controller.rb b/app/controllers/datahub/upvs/public_authority_edesks_controller.rb
index fcf56499..3ab11c89 100644
--- a/app/controllers/datahub/upvs/public_authority_edesks_controller.rb
+++ b/app/controllers/datahub/upvs/public_authority_edesks_controller.rb
@@ -1,8 +1,9 @@
-module Datahub::Upvs
- class PublicAuthorityEdesksController < ApplicationController
-
- def search
- render json: PublicAuthorityEdesk.search(params[:q])
+module Datahub
+ module Upvs
+ class PublicAuthorityEdesksController < ApplicationController
+ def search
+ render json: PublicAuthorityEdesk.search(params[:q])
+ end
end
end
end
diff --git a/app/controllers/datahub/upvs/services_with_forms_controller.rb b/app/controllers/datahub/upvs/services_with_forms_controller.rb
new file mode 100644
index 00000000..7b02b94f
--- /dev/null
+++ b/app/controllers/datahub/upvs/services_with_forms_controller.rb
@@ -0,0 +1,26 @@
+module Datahub
+ module Upvs
+ class ServicesWithFormsController < ApplicationController
+ def search_recipient
+ result = if testing?
+ # Datahub::Upvs::ServicesWithForms.search(params[:q])
+ [{ name: 'Testing - ico://sk/83369507', uri: 'ico://sk/83369507' }]
+ else
+ Datahub::Upvs::ServicesWithForms.search(params[:q])
+ end
+ render json: { result: result }
+ end
+
+ private
+
+ # Testing is determined based on the content of 'SLOVENSKO_SK_API_URL' which looks like e.g.:
+ # - https://localhost:4000
+ # - https://fix.slovensko-sk-api.staging.slovensko.digital
+ #
+ # If you need to see production list of recipients, change the `SLOVENSKO_SK_API_URL` ENV to something else
+ def testing?
+ ['staging.', 'localhost'].any? { |e| ENV['SLOVENSKO_SK_API_URL'].to_s.include?(e) }
+ end
+ end
+ end
+end
diff --git a/app/controllers/upvs/submissions_controller.rb b/app/controllers/upvs/submissions_controller.rb
index af6b9232..2dce0219 100644
--- a/app/controllers/upvs/submissions_controller.rb
+++ b/app/controllers/upvs/submissions_controller.rb
@@ -3,7 +3,8 @@ class Upvs::SubmissionsController < ApplicationController
before_action :set_metadata
before_action :build_upvs_submission, only: :create
- before_action :load_upvs_submission, only: [:show, :submit, :finish]
+ before_action :load_upvs_submission, only: [:show, :submit, :finish, :signing_data, :update_blob_after_signature]
+ before_action :load_blob, only: [:signing_data, :update_blob_after_signature]
rescue_from Upvs::Submission::SkApiError, :with => :handle_error
@@ -52,8 +53,54 @@ def finish
def submission_error
end
+ def signing_data
+ response = {
+ file_name: @blob.filename,
+ mime_type: @blob.content_type,
+ content: Base64.strict_encode64(@blob.download)
+ }
+
+ # This mime type is set in `UpvsSubmissions::Forms::GeneralAgenda#create_form_attachment`
+ if @blob.content_type == 'application/x-eform-xml'
+ form_template = Upvs::FormTemplateRelatedDocument.find_by!(message_type: 'App.GeneralAgenda')
+
+ response.merge!({
+ identifier: form_template.identifier,
+ container_xmlns: 'http://data.gov.sk/def/container/xmldatacontainer+xml/1.1',
+ schema: Base64.strict_encode64(form_template.xsd_schema),
+ transformation: Base64.strict_encode64(form_template.xslt_transformation)
+ })
+ end
+
+ render json: response
+ end
+
+ def update_blob_after_signature
+ render_partial = params[:render_partial].in?(['signed_badge', 'blob_row_content']) ? params[:render_partial] : 'signed_badge'
+ new_blob = ActiveStorage::Blob.create_and_upload!(
+ io: StringIO.new(Base64.decode64(params[:content])),
+ filename: params[:name],
+ content_type: params[:mimetype],
+ metadata: { signed: true, signed_required: @blob.metadata[:signed_required] }.compact
+ )
+
+ @blob.signed_blob.attach(new_blob) # 'signed_blob' is defined in `config/initializers/active_storage.rb`
+
+ render json: {
+ success: true,
+ old_blob_id: @blob.id,
+ html: render_to_string(partial: "upvs/submissions/#{render_partial}", locals: { blob: @blob })
+ }
+ rescue StandardError, ScriptError => e
+ render json: { success: false, error: "Nastala chyba pri podpisovaní. [#{e.class}]: #{e.message}" }
+ end
+
private
+ def load_blob
+ @blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id]) # This is only 'hashed blob ID', not signed blob object nor file
+ end
+
def load_upvs_submission
@upvs_submission = current_user.find_upvs_submission!(params[:id] || params[:submission_id])
end
@@ -87,9 +134,10 @@ def submission_params
:sender_business_reference,
:recipient_business_reference,
:message_subject,
- :form,
+ :form_blob_id,
:attachments,
- :callback_url
+ :callback_url,
+ attachments_ids: []
).except(:authenticity_token)
end
diff --git a/app/helpers/app_form_builder.rb b/app/helpers/app_form_builder.rb
index 375d57da..9b5eaf83 100644
--- a/app/helpers/app_form_builder.rb
+++ b/app/helpers/app_form_builder.rb
@@ -30,6 +30,36 @@ def text_field(method, options = {})
end
end
+ def text_area(method, options = {})
+ group_classes = ['govuk-form-group']
+ field_classes = ['govuk-textarea', options[:class]]
+ described_by = []
+
+ label = options.delete(:label)
+ label = label(method, label, class: 'govuk-label') if label
+
+ hint = options.delete(:hint)
+ if hint
+ hint = @template.content_tag(:span, hint, id: hint_id(method), class: 'govuk-hint')
+ described_by << hint_id(method)
+ end
+
+ if @object.errors[method].present?
+ group_classes << 'govuk-form-group--error'
+ field_classes << 'govuk-input--error'
+ described_by << error_id(method)
+ end
+
+ options = options.merge({'aria-describedby': described_by.join(' ')}) unless described_by.empty?
+
+ @template.content_tag(:div, class: group_classes) do
+ @template.concat label
+ @template.concat hint
+ @template.concat error_message(method)
+ @template.concat super(method, objectify_options(options.merge({class: field_classes})))
+ end
+ end
+
def select(method, values, options = {}, html_options = {}, &block)
group_classes = ['govuk-form-group']
field_classes = ['govuk-select', html_options[:class]]
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 76d78d9e..022e948c 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -18,4 +18,10 @@ def sanitize_description(description, length: 500)
def dont_show_small_search_bar?
current_page?(root_path) || current_page?(search_path)
end
+
+ def localize_boolean(value)
+ value.to_s.in?(['1', 'true']) ? 'Áno' : 'Nie'
+ end
+
+ alias :lb :localize_boolean
end
diff --git a/app/jobs/remove_stale_blobs_job.rb b/app/jobs/remove_stale_blobs_job.rb
new file mode 100644
index 00000000..568771bd
--- /dev/null
+++ b/app/jobs/remove_stale_blobs_job.rb
@@ -0,0 +1,11 @@
+class RemoveStaleBlobs < ApplicationJob
+ queue_as :default
+
+ # TODO: Setup this as a cron job and run once a week
+ def perform
+ # In `GeneralAgenda` user can upload a file which creates an `ActiveStorage::Blob`
+ # if this form is not converted into `Uvps::Submission`, the file remains uploaded, but not attached anywhere
+ # These files should be regularly purged
+ ActiveStorage::Blob.unattached.where('created_at < ?', 3.days.ago).each(&:purge_later)
+ end
+end
diff --git a/app/lib/analyzers/signed_pdf_analyzer.rb b/app/lib/analyzers/signed_pdf_analyzer.rb
new file mode 100644
index 00000000..8693ce88
--- /dev/null
+++ b/app/lib/analyzers/signed_pdf_analyzer.rb
@@ -0,0 +1,21 @@
+module Analyzers
+ class SignedPdfAnalyzer < ActiveStorage::Analyzer
+ def self.accept?(blob)
+ ['application/pdf'].include? blob.content_type
+ end
+
+ def metadata
+ { is_signed: is_signed? }
+ end
+
+ def is_signed?
+ begin
+ reader = PDF::Reader.new(blob.download)
+ rescue StandardError
+ return false # NOTE: if pdf reading fails it is not signed
+ end
+
+ reader.objects.to_a.flatten.select { |o| o.is_a?(Hash) }.select { |o| o[:Type] == :Sig }.first.present?
+ end
+ end
+end
diff --git a/app/lib/analyzers/signed_x_analyzer.rb b/app/lib/analyzers/signed_x_analyzer.rb
new file mode 100644
index 00000000..d70647c5
--- /dev/null
+++ b/app/lib/analyzers/signed_x_analyzer.rb
@@ -0,0 +1,15 @@
+module Analyzers
+ class SignedXAnalyzer < ActiveStorage::Analyzer
+ def self.accept?(blob)
+ ['application/vnd.etsi.asic-e+zip'].include? blob.content_type
+ end
+
+ def metadata
+ { is_signed: is_signed? }
+ end
+
+ def is_signed?
+ true # NOTE: this content type is always signed
+ end
+ end
+end
diff --git a/app/lib/upvs_submissions/form_builders/general_agenda_form_builder.rb b/app/lib/upvs_submissions/form_builders/general_agenda_form_builder.rb
new file mode 100644
index 00000000..1675ac76
--- /dev/null
+++ b/app/lib/upvs_submissions/form_builders/general_agenda_form_builder.rb
@@ -0,0 +1,17 @@
+module UpvsSubmissions
+ module FormBuilders
+ class GeneralAgendaFormBuilder
+ def self.build_form(form)
+ xml_form = Nokogiri::XML::Builder.new do |doc|
+ doc.GeneralAgenda('xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xmlns' => 'http://schemas.gov.sk/form/App.GeneralAgenda/1.9') do
+ doc.subject form.subject
+ doc.text_ form.text
+ end
+ end
+
+ xml_form.doc.root
+ end
+ end
+ end
+end
diff --git a/app/lib/upvs_submissions/forms/application_for_document_copy.rb b/app/lib/upvs_submissions/forms/application_for_document_copy.rb
index 7843c671..9f680274 100644
--- a/app/lib/upvs_submissions/forms/application_for_document_copy.rb
+++ b/app/lib/upvs_submissions/forms/application_for_document_copy.rb
@@ -3,7 +3,7 @@ module Forms
class ApplicationForDocumentCopy
include ActiveModel::Model
- attr_accessor :sender_uri, :recipient_uri, :sender_business_reference, :recipient_business_reference, :form, :attachments
+ attr_accessor :sender_uri, :recipient_uri, :sender_business_reference, :recipient_business_reference, :form_blob_id, :attachments
class << self
delegate :uuid, to: SecureRandom
@@ -14,10 +14,22 @@ def initialize(recipient_uri: nil, sender_uri: nil, sender_business_reference: n
@recipient_uri = recipient_uri || default_recipient_uri
@sender_business_reference = sender_business_reference
@recipient_business_reference = recipient_business_reference
- @form = form_params ? build_form(form_params) : nil
@attachments = []
end
+ def self.create_form_attachment(application_form)
+ document_xml = UpvsSubmissions::FormBuilders::ApplicationForDocumentCopyFormBuilder.build_form(application_form)
+
+ blob = ActiveStorage::Blob.create_and_upload!(
+ io: StringIO.new(document_xml.to_xml),
+ filename: 'Dokument.xml', # This is how the XML is called in slovensko.sk
+ content_type: 'application/x-eform-xml', # Mandatory content type for the Autogram and UPVS app. See `Upvs::SubmissionsController#signing_data`
+ metadata: { signed_required: false }
+ )
+
+ blob.id
+ end
+
def posp_id
"00166073.MSSR_ORSR_Poziadanie_o_vyhotovenie_kopie_listiny_ulozenej_v_zbierke_listin.sk"
end
diff --git a/app/lib/upvs_submissions/forms/general_agenda.rb b/app/lib/upvs_submissions/forms/general_agenda.rb
new file mode 100644
index 00000000..4d4e858d
--- /dev/null
+++ b/app/lib/upvs_submissions/forms/general_agenda.rb
@@ -0,0 +1,66 @@
+module UpvsSubmissions
+ module Forms
+ class GeneralAgenda
+ include ActiveModel::Model
+
+ attr_accessor :application_form, :form_blob_id, :attachments
+
+ delegate :recipient_uri, to: :application_form
+
+ class << self
+ delegate :uuid, to: SecureRandom
+ end
+
+ def initialize(application_form)
+ @application_form = application_form
+ @form_blob_id = create_form_attachment
+ @attachments = []
+ end
+
+ def posp_id
+ "App.GeneralAgenda"
+ end
+
+ def posp_version
+ "1.9"
+ end
+
+ def message_type
+ "App.GeneralAgenda"
+ end
+
+ def message_subject
+ "Všeobecná agenda"
+ end
+
+ def message_id
+ @message_id ||= uuid
+ end
+
+ def correlation_id
+ @correlation_id ||= uuid
+ end
+
+ def attachments_blob_ids
+ application_form.attachments.to_h.values.map(&:to_i)
+ end
+
+ private
+
+ def create_form_attachment
+ document_xml = UpvsSubmissions::FormBuilders::GeneralAgendaFormBuilder.build_form(application_form)
+
+ blob = ActiveStorage::Blob.create_and_upload!(
+ io: StringIO.new(document_xml.to_xml),
+ filename: 'Dokument.xml', # This is how the XML is called in slovensko.sk
+ content_type: 'application/x-eform-xml', # Mandatory content type for the Autogram and UPVS app. See `Upvs::SubmissionsController#signing_data`
+ metadata: { signed_required: application_form.signed_required? }
+ )
+
+ blob.id
+ end
+
+ delegate :uuid, to: self
+ end
+ end
+end
diff --git a/app/lib/upvs_submissions/sktalk_message_builder.rb b/app/lib/upvs_submissions/sktalk_message_builder.rb
index 5e766f07..9bed139a 100644
--- a/app/lib/upvs_submissions/sktalk_message_builder.rb
+++ b/app/lib/upvs_submissions/sktalk_message_builder.rb
@@ -5,7 +5,6 @@ class << self
XML_ENTITIES = HTMLEntities.new(:expanded)
- # TODO add #{build_attachment_objects(egov_application.attachments)} when attachments added to upvs_submission
def build_sktalk_message(egov_application, eid_token)
<<~SKTALK
@@ -27,7 +26,7 @@ def build_sktalk_message(egov_application, eid_token)
#{egov_application.recipient_uri}
#{egov_application.message_type}
#{sanitize(egov_application.message_subject)}
- #{build_business_references(egov_application) if references_present?(egov_application)}#{build_form_object(egov_application.form)}
+ #{build_business_references(egov_application) if references_present?(egov_application)}#{build_form_object(egov_application.form)}#{build_attachment_objects(egov_application.attachments)}