# frozen_string_literal: true require "cgi" module SecureCookies class CookiesConfigError < StandardError; end class Cookie attr_reader :raw_cookie, :config COOKIE_DEFAULTS = { httponly: true, secure: true, samesite: { lax: true }, }.freeze def initialize(cookie, config) @raw_cookie = cookie unless config == OPT_OUT config ||= {} config = COOKIE_DEFAULTS.merge(config) end @config = config @attributes = { httponly: nil, samesite: nil, secure: nil, } parse(cookie) end def to_s @raw_cookie.dup.tap do |c| c << "; secure" if secure? c << "; HttpOnly" if httponly? c << "; #{samesite_cookie}" if samesite? end end def secure? flag_cookie?(:secure) && !already_flagged?(:secure) end def httponly? flag_cookie?(:httponly) && !already_flagged?(:httponly) end def samesite? flag_samesite? && !already_flagged?(:samesite) end private def parsed_cookie @parsed_cookie ||= CGI::Cookie.parse(raw_cookie) end def already_flagged?(attribute) @attributes[attribute] end def flag_cookie?(attribute) return false if config == OPT_OUT case config[attribute] when TrueClass true when Hash conditionally_flag?(config[attribute]) else false end end def conditionally_flag?(configuration) if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?) true elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?) true else false end end def samesite_cookie if flag_samesite_lax? "SameSite=Lax" elsif flag_samesite_strict? "SameSite=Strict" end end def flag_samesite? return false if config == OPT_OUT || config[:samesite] == OPT_OUT flag_samesite_lax? || flag_samesite_strict? end def flag_samesite_lax? flag_samesite_enforcement?(:lax) end def flag_samesite_strict? flag_samesite_enforcement?(:strict) end def flag_samesite_enforcement?(mode) return unless config[:samesite] if config[:samesite].is_a?(TrueClass) && mode == :lax return true end case config[:samesite][mode] when Hash conditionally_flag?(config[:samesite][mode]) when TrueClass true else false end end def parse(cookie) return unless cookie cookie.split(/[;,]\s?/).each do |pairs| name, values = pairs.split("=", 2) name = CGI.unescape(name) attribute = name.downcase.to_sym if @attributes.has_key?(attribute) @attributes[attribute] = values || true end end end end end