app/controllers/quby/answers_controller.rb
# frozen_string_literal: true
# -*- coding: utf-8 -*-
require 'addressable/uri'
require 'browser'
module Quby
class AnswersController < Quby::ApplicationController
DISPLAY_MODES = %w(paged bulk single_page).freeze
before_action :load_token_and_hmac_and_timestamp
before_action :load_return_url_and_token
before_action :load_custom_stylesheet
before_action :load_display_mode
before_action :verify_answer_id_against_session
before_action :verify_hmac, only: [:edit, :pdf]
before_action :find_questionnaire
before_action :check_questionnaire_valid
before_action :find_answer
before_action :verify_token, only: [:show, :edit, :update]
before_action :check_aborted, only: :update
rescue_from TokenValidationError, with: :bad_authorization
rescue_from TimestampValidationError, with: :bad_authorization
rescue_from TimestampExpiredError, with: :bad_authorization
rescue_from InvalidAuthorizationError, with: :bad_authorization
rescue_from MissingAuthorizationError, with: :bad_authorization
rescue_from Quby::Questionnaires::Repos::QuestionnaireNotFound, with: :bad_questionnaire
rescue_from InvalidQuestionnaireDefinitionError, with: :bad_questionnaire_definition
rescue_from Quby::Questionnaires::Entities::Questionnaire::ValidationError, with: :bad_questionnaire_definition
def show
redirect_to action: "edit"
end
def edit
default_to_textvar_values(@answer)
render versioned_template_options(@display_mode)
end
def update
if session[:has_downloaded_pdf_for] == @answer.id && defined?(Appsignal) && on_ios_safari?
Appsignal.increment_counter("ios_safari_downloaded_pdf_and_pressed_done", 1)
end
update_or_fail do
if @return_url.blank?
render versioned_template_options("completed", layout: request.xhr? ? "content_only" : 'application')
else
redirect_url = return_url(status: 'updated', go: form_action)
request.xhr? ?
render(js: "window.location = '#{redirect_url}'") :
redirect_to(redirect_url)
end
end
end
def pdf
if defined?(Appsignal) && on_ios_safari?
session[:has_downloaded_pdf_for] = @answer.id
Appsignal.increment_counter("ios_safari_downloaded_pdf", 1)
end
update_or_fail do
template_string = render_to_string versioned_template_options("print", layout: "pdf")
begin
pdf_binary = Quby::PdfRenderer.render_pdf(template_string)
# type is not a application/pdf, to prevent previews on ios
send_data pdf_binary, filename: "#{@questionnaire.title} #{Time.zone.now.to_s(:filename)}.pdf",
type: 'application/octet-stream', disposition: :attachment
rescue StandardError
flash.now[:notice] = I18n.t('pdf_download_failed_message')
render versioned_template_options(@display_mode, layout: request.xhr? ? "content_only" : 'application')
end
end
end
def bad_authorization(exception)
if @return_url
redirect_to return_url(status: 'error', error: exception.class.to_s)
else
@error = "U probeert een vragenlijst te openen waar u geen toegang toe heeft op dit moment."
render template: 'quby/errors/generic', layout: 'quby/dialog'
handle_exception exception
end
end
def bad_questionnaire(exception)
if @return_url
redirect_to return_url(status: 'error', error: exception.class.to_s)
else
@error = exception
render template: "quby/errors/questionnaire_not_found", layout: "quby/dialog", status: 404
end
end
def bad_questionnaire_definition(exception)
if Quby.show_exceptions
render action: :show_questionnaire_errors
elsif @return_url
redirect_to return_url(status: 'error', error: exception.class.to_s)
handle_exception exception
else
@error = "Er is iets mis met de vragenlijst zoals deze in ons systeem is ingebouwd."
render template: 'quby/errors/generic', layout: 'quby/dialog'
handle_exception exception
end
end
protected
def update_or_fail
updater = Answers::Services::UpdatesAnswers.new(@answer)
updater.on_success do
yield
end
updater.on_failure do
flash.now[:notice] = "De vragenlijst is nog niet volledig ingevuld." if @display_mode == "paged"
render versioned_template_options(@display_mode, layout: request.xhr? ? "content_only" : 'application')
end
updater.update((params[:answer] || {}).merge("rendered_at" => params[:rendered_at]))
end
def find_questionnaire
if params[:questionnaire_id]
@questionnaire = Quby.questionnaires.find(params[:questionnaire_id])
end
end
def check_questionnaire_valid
# don't use valid?, since it clears the errors
return if @questionnaire.errors.size == 0
fail InvalidQuestionnaireDefinitionError
end
def find_answer
@answer = Quby.answers.find(@questionnaire.key, params[:id])
end
def check_aborted
if (params[:abort] && @questionnaire.abortable) ||
(params[:save_anyway] && (@display_mode == "bulk" || @display_mode == "single_page")) ||
(params[:previous_questionnaire])
params[:answer] ||= HashWithIndifferentAccess.new
params[:answer][:aborted] = true
else
params[:answer] ||= HashWithIndifferentAccess.new
params[:answer][:aborted] = false
end
end
def verify_answer_id_against_session
if Quby::Settings.authorize_with_id_from_session
fail MissingAuthorizationError unless session[:quby_answer_id].present?
fail InvalidAuthorizationError unless params[:id].to_s == session[:quby_answer_id].to_s
end
end
def verify_token
if Quby::Settings.authorize_with_hmac
fail InvalidAuthorizationError unless @answer.token == (params[:token] || @answer_token)
end
end
def verify_hmac # rubocop:disable CyclomaticComplexity
if Quby::Settings.authorize_with_hmac
fail TokenValidationError, "No HMAC secret is configured" unless Quby::Settings.shared_secret.present?
hmac = (params['hmac'] || @hmac || '').strip
token = (params['token'] || @answer_token || '').strip
timestamp = (params['timestamp'] || @timestamp || '').strip
current_hmac = calculate_hmac(Quby::Settings.shared_secret, token, timestamp)
if Quby::Settings.previous_shared_secret.present?
previous_hmac = calculate_hmac(Quby::Settings.previous_shared_secret, token, timestamp)
end
unless timestamp =~ /^\d\d\d\d-?\d\d-?\d\d[tT ]?\d?\d:?\d\d/ and time = Time.parse(timestamp)
logger.error "ERROR::Authentication error: Invalid timestamp."
fail TimestampValidationError
end
if time < 24.hours.ago or 1.hour.since < time
logger.error "ERROR::Authentication error: Request expired"
fail TimestampExpiredError
end
if current_hmac != hmac && (previous_hmac.blank? || previous_hmac != hmac)
logger.error "ERROR::Authentication error: Token does not validate"
fail TokenValidationError, "HMAC"
end
end
end # rubocop:enable CyclomaticComplexity
def load_token_and_hmac_and_timestamp
@answer_token = params[:token] if params[:token]
@hmac = params[:hmac] if params[:hmac]
@timestamp = params[:timestamp] if params[:timestamp]
end
def load_return_url_and_token
if params[:return_url]
@return_url = CGI.unescape(params[:return_url])
@return_token = params[:return_token]
end
end
def load_display_mode
@display_mode = params[:display_mode] if DISPLAY_MODES.include? params[:display_mode]
@display_mode ||= 'paged'
end
def load_custom_stylesheet
@custom_stylesheet = params[:stylesheet]
end
def versioned_template_options(template_name, options = {})
{template: "quby/#{@questionnaire.renderer_version}/#{template_name}",
layout: "quby/#{@questionnaire.renderer_version}/layouts/#{options.fetch(:layout, "application")}"}
end
def form_action
if params[:abort]
'stop'
elsif params[:previous_questionnaire]
'back'
else
'next'
end
end
def return_url(options = {})
address = Addressable::URI.parse(@return_url)
# Addressable behaves strangely if were to do this directly on
# it's own hash, hence the (otherwise unneeded) temporary variable
query_values = (address.query_values || {})
query_values.merge!(key: @return_token, return_from: "quby")
query_values.merge!(return_from_answer: params[:id])
options.each { |key, value| query_values[key] = value if value }
address.query_values = query_values
logger.info address.to_s
address.to_s
end
def calculate_hmac(*args)
Digest::SHA1.hexdigest(args.join('|'))
end
def default_to_textvar_values(answer)
@questionnaire.questions.each do |question|
textvar = question.sets_textvar or next
if answer.textvars.key?(textvar.to_sym) && !answer.value.key?(question.key.to_s)
answer.value[question.key.to_s] ||= answer.textvars[textvar.to_sym]
end
end
end
# I18n.t based on the questionnaire's configuration.
# This is done instead of configuring I18n.locale to allow for example
# English questionnaires within a Dutch RoQua.
def translate(key, options = {})
I18n.t(key, options.merge(locale: @questionnaire.language))
end
helper_method :translate
def on_ios_safari?
browser = Browser.new(request.user_agent)
browser.safari? && browser.platform.ios?
end
end
end