# frozen_string_literal: true require "coverband" begin require "rack" rescue LoadError puts "error loading Coverband web reporter as Rack is not available" end module Coverband module Reporters class Web attr_reader :request CSP_HEADER = [ "default-src 'self' https: http:", "child-src 'self'", "connect-src 'self' https: http: wss: ws:", "font-src 'self' https: http:", "frame-src 'self'", "img-src 'self' https: http: data:", "manifest-src 'self'", "media-src 'self'", "object-src 'none'", "script-src 'self' https: http: 'unsafe-inline'", "style-src 'self' https: http: 'unsafe-inline'", "worker-src 'self'", "base-uri 'self'" ].join("; ").freeze def init_web full_path = Gem::Specification.find_by_name("coverband").full_gem_path @static = Rack::Static.new(self, root: File.expand_path("public", full_path), urls: [/.*\.css/, /.*\.js/, /.*\.gif/, /.*\.png/]) end def check_auth return true unless Coverband.configuration.password # support rack 1.6.x and rack 2.0 (get_header) auth_header = request.respond_to?(:get_header) ? request.get_header("HTTP_AUTHORIZATION") : request.env["HTTP_AUTHORIZATION"] return unless auth_header Coverband.configuration.password == Base64.decode64(auth_header.split[1]).split(":")[1] end def call(env) @request = Rack::Request.new(env) return [401, {"www-authenticate" => 'Basic realm=""'}, [""]] unless check_auth request_path_info = request.path_info == "" ? "/" : request.path_info tracker_route = false Coverband.configuration.trackers.each do |tracker| if request_path_info.match(tracker.class::REPORT_ROUTE) tracker_route = true if request_path_info =~ %r{\/clear_.*_key} return clear_abstract_tracking_key(tracker) elsif request_path_info =~ %r{\/clear_.*} return clear_abstract_tracking(tracker) else return [200, {"Content-Type" => "text/html"}, [display_abstract_tracker(tracker)]] end end end unless tracker_route if request.post? case request_path_info when %r{\/clear_file} clear_file when %r{\/clear} clear else [404, coverband_headers, ["404 error!"]] end else case request_path_info when /.*\.(css|js|gif|png)/ @static.call(env) when %r{\/settings} [200, coverband_headers, [settings]] when %r{\/view_tracker_data} [200, coverband_headers(content_type: "text/json"), [view_tracker_data]] when %r{\/enriched_debug_data} [200, coverband_headers(content_type: "text/json"), [enriched_debug_data]] when %r{\/debug_data} [200, coverband_headers(content_type: "text/json"), [debug_data]] when %r{\/load_file_details} [200, coverband_headers(content_type: "text/json"), [load_file_details]] when %r{\/json} [200, coverband_headers(content_type: "text/json"), [json]] when %r{\/$} [200, coverband_headers, [index]] else [404, coverband_headers, ["404 error!"]] end end end end def index notice = "Notice: #{Rack::Utils.escape_html(request.params["notice"])}
" notice = request.params["notice"] ? notice : "" Coverband::Reporters::HTMLReport.new(Coverband.configuration.store, static: false, base_path: base_path, notice: notice, open_report: false).report end def json Coverband::Reporters::JSONReport.new(Coverband.configuration.store).report end def settings Coverband::Utils::HTMLFormatter.new(nil, base_path: base_path).format_settings! end def display_abstract_tracker(tracker) notice = "Notice: #{Rack::Utils.escape_html(request.params["notice"])}
" notice = request.params["notice"] ? notice : "" Coverband::Utils::HTMLFormatter.new(nil, tracker: tracker, notice: notice, base_path: base_path).format_abstract_tracker! end def view_tracker_data Coverband::Collectors::ViewTracker.new.as_json end def debug_data Coverband.configuration.store.get_coverage_report.to_json end def enriched_debug_data Coverband::Reporters::HTMLReport.new(Coverband.configuration.store, static: false, base_path: base_path, notice: "", open_report: false).report_data end def load_file_details filename = request.params["filename"] Coverband::Reporters::HTMLReport.new(Coverband.configuration.store, filename: filename, base_path: base_path, open_report: false).file_details end def clear if Coverband.configuration.web_enable_clear Coverband.configuration.store.clear! notice = "coverband coverage cleared" else notice = "web_enable_clear isn't enabled in your configuration" end [302, {"Location" => "#{base_path}?notice=#{notice}"}, []] end def clear_file if Coverband.configuration.web_enable_clear filename = request.params["filename"] Coverband.configuration.store.clear_file!(filename) notice = "coverage for file #{filename} cleared" else notice = "web_enable_clear isn't enabled in your configuration" end [302, {"Location" => "#{base_path}?notice=#{notice}"}, []] end def clear_abstract_tracking(tracker) if Coverband.configuration.web_enable_clear tracker.reset_recordings notice = "#{tracker.title} tracking reset" else notice = "web_enable_clear isn't enabled in your configuration" end [302, {"Location" => "#{base_path}/#{tracker.route}?notice=#{notice}"}, []] end def clear_abstract_tracking_key(tracker) if Coverband.configuration.web_enable_clear key = request.params["key"] tracker.clear_key!(key) notice = "coverage for #{tracker.title} #{key} cleared" else notice = "web_enable_clear isn't enabled in your configuration" end [302, {"Location" => "#{base_path}/#{tracker.route}?notice=#{notice}"}, []] end private def coverband_headers(content_type: "text/html") web_headers = { "Content-Type" => content_type } web_headers["Content-Security-Policy-Report-Only"] = CSP_HEADER if Coverband.configuration.csp_policy web_headers end # This method should get the root mounted endpoint # for example if the app is mounted like so: # mount Coverband::Web, at: '/coverage' # "/coverage/collect_coverage?" become: # /coverage/ # NOTE: DO NOT let standardrb `autofix` this to regex match # %r{\/.*\/}.match?(request.path) ? request.path.match("\/.*\/")[0] : "/" # ^^ the above is NOT valid Ruby 2.3/2.4 even though rubocop / standard think it is def base_path request.path =~ %r{\/.*\/} ? request.path.match("/.*/")[0] : "/" end end end end