module SecureHeaders
  module Configuration
    class << self
      attr_accessor :hsts, :x_frame_options, :x_content_type_options,
        :x_xss_protection, :csp

      def configure &block
        instance_eval &block
      end
    end
  end

  class << self
    def append_features(base)
      base.module_eval do
        @@secure_headers_options = nil

        extend ClassMethods
        include InstanceMethods

        # jank?
        def self.secure_headers_options=(opts)
          @@secure_headers_options = opts
        end

        def self.secure_headers_options
          @@secure_headers_options
        end
      end
    end
  end

  module ClassMethods
    def ensure_security_headers options = {}, *args
      self.secure_headers_options = options
      before_filter :set_security_headers
    end

    # we can't use ||= because I'm overloading false => disable, nil => default
    # both of which trigger the conditional assignment
    def options_for(type, options)
      options.nil? ? ::SecureHeaders::Configuration.send(type) : options
    end
  end

  module InstanceMethods
    def set_security_headers(options = self.class.secure_headers_options || {})
      brwsr = Brwsr::Browser.new(:ua => request.env['HTTP_USER_AGENT'])
      set_hsts_header(options[:hsts]) if request.ssl?
      set_x_frame_options_header(options[:x_frame_options])
      set_csp_header(request, options[:csp]) unless broken_implementation?(brwsr)
      set_x_xss_protection_header(options[:x_xss_protection])
      if brwsr.ie?
        set_x_content_type_options_header(options[:x_content_type_options])
      end
    end

    def set_csp_header(request, options=nil)
      options = self.class.options_for :csp, options
      return if options == false

      header = ContentSecurityPolicy.new(request, options)
      set_header(header.name, header.value)
      if options && options[:experimental] && options[:enforce]
        header = ContentSecurityPolicy.new(request, options, :experimental => true)
        set_header(header.name, header.value)
      end
    end

    def set_a_header(name, klass, options=nil)
      options = self.class.options_for name, options
      return if options == false

      header = klass.new(options)
      set_header(header.name, header.value)
    end

    def set_x_frame_options_header(options=nil)
      set_a_header(:x_frame_options, XFrameOptions, options)
    end

    def set_x_content_type_options_header(options=nil)
      set_a_header(:x_content_type_options, XContentTypeOptions, options)
    end

    def set_x_xss_protection_header(options=nil)
      set_a_header(:x_xss_protection, XXssProtection, options)
    end

    def set_hsts_header(options=nil)
      set_a_header(:hsts, StrictTransportSecurity, options)
    end

    def set_header(name, value)
      response.headers[name] = value
    end

    private

    def broken_implementation?(browser)
      #IOS 5 sometimes refuses to load external resources even when whitelisted with CSP
      return browser.ios5?
    end
  end
end


require "secure_headers/version"
require "secure_headers/headers/content_security_policy"
require "secure_headers/headers/x_frame_options"
require "secure_headers/headers/strict_transport_security"
require "secure_headers/headers/x_xss_protection"
require "secure_headers/headers/x_content_type_options"
require "secure_headers/railtie"
require "brwsr"