# frozen_string_literal: true require 'open3' require 'base64' require 'securerandom' class Captcha attr_reader :image_base64, :solution, :solution_id NUMBERS = %w[1 2 3 4 5 6].freeze IMAGES = %w[1 2 3 4 5 6].freeze CAPTCHA_ASSETS_BASE_PATH = 'app/assets/images/captcha' NUMBER_FILES = { '1' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '1.png').to_s, '2' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '2.png').to_s, '3' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '3.png').to_s, '4' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '4.png').to_s, '5' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '5.png').to_s, '6' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '6.png').to_s }.freeze IMAGE_FILES = { '1' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i1.png').to_s, '2' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i2.png').to_s, '3' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i3.png').to_s, '4' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i4.png').to_s, '5' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i5.png').to_s, '6' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i6.png').to_s }.freeze BACKGROUND_FILE = Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'background.png').to_s GEOMETRY = { 1 => '+0+0', 2 => '+120+0', 3 => '+240+0', 4 => '+0+120', 5 => '+120+120', 6 => '+240+120', 7 => '+0+240', 8 => '+120+240', 9 => '+240+240' }.freeze DISTORTION_1 = [ %w[-implode .1].freeze, %w[-implode -.1].freeze ].freeze DISTORTION_2 = [ %w[-swirl 10].freeze, %w[-swirl -10].freeze, %w[-swirl 20].freeze, %w[-swirl -20].freeze ].freeze DISTORTION_3 = [ %w[-wave 5x180].freeze, %w[-wave 5x126].freeze, %w[-wave 10x180].freeze, %w[-wave 10x126].freeze ].freeze def initialize @solution = NUMBERS.zip(IMAGES.shuffle).to_h # 3x3 render grid grid = [*NUMBERS, nil, nil, nil].shuffle # Base arguments imagemagick_cmd = [ '-page', '360x660', BACKGROUND_FILE ] # Add individual grid tiles grid.each_with_index do |num, i| next if num.nil? imagemagick_cmd += [IMAGE_FILES[@solution[num]], '-geometry', GEOMETRY[i + 1], '-composite'] imagemagick_cmd += [NUMBER_FILES[num], '-geometry', GEOMETRY[i + 1], '-composite'] end imagemagick_cmd += [DISTORTION_1.sample, DISTORTION_2.sample, DISTORTION_3.sample].shuffle.flatten imagemagick_cmd += ['-quality', '8', 'jpeg:-'] image_data, = Open3.capture2('convert', *imagemagick_cmd) @image_base64 = Base64.encode64(image_data) # Store solution in redis so that we can prevent reuse @solution_id = "cp#{SecureRandom.hex(12)}" $redis.setex(@solution_id, 10.minutes.to_i, @solution.to_json) end def self.validate(solution_id, solution) # Delete key immediately. This may race, but the # impact should be minimal if a race succeeds. sol = $redis.get(solution_id) $redis.del(solution_id) return false if sol.blank? sol = JSON.parse(sol) # Ruby hash equality tests key/value equality, but not # key order equality, so this works. sol == solution rescue StandardError false end def self.options @@options ||= NUMBERS.map { |n| [I18n.translate("captcha.solutions.#{n}"), n] }.freeze end end