substancelab/route_downcaser

View on GitHub
lib/route_downcaser/downcase_route_middleware.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
# frozen_string_literal: true

require "active_support/core_ext/object/blank"

module RouteDowncaser
  class DowncaseRouteMiddleware
    def initialize(app)
      @app = app
    end

    def call(env)
      dup._call(env)
    end

    def _call(env)
      request_uri = env["REQUEST_URI"]
      path_info = env["PATH_INFO"]

      # Don't touch anything, if uri/path is part of exclude_patterns
      return @app.call(env) if excluded?([request_uri, path_info])

      # Downcase request_uri and/or path_info if applicable
      request_uri = downcased_uri(request_uri)
      path_info = downcased_uri(path_info)

      # If redirect configured, then return redirect request,
      # if either request_uri or path_info has changed
      if RouteDowncaser.redirect && env["REQUEST_METHOD"] == "GET"
        return redirect_header(request_uri) if request_uri.present? && request_uri != env["REQUEST_URI"]

        return redirect_header(path_info) if path_info.present? && path_info != env["PATH_INFO"]
      end

      env["PATH_INFO"] = path_info.to_s if path_info
      env["REQUEST_URI"] = request_uri.to_s if request_uri

      # Default just move to next chain in Rack callstack
      # calling with downcased uri if needed
      @app.call(env)
    end

    private

    def exclude_patterns_match?(uri)
      uri.match(Regexp.union(RouteDowncaser.exclude_patterns)) if uri && RouteDowncaser.exclude_patterns
    end

    def excluded?(paths)
      paths.any? do |path|
        exclude_patterns_match?(path)
      end
    end

    def downcased_uri(uri)
      return nil unless uri.present?

      if has_querystring?(uri)
        "#{downcased_path(uri)}?#{querystring(uri)}"
      else
        downcased_path(uri)
      end
    end

    def downcased_path(uri)
      path(uri).split("/").map(&method(:downcase_path_segment)).join("/")
    end

    def downcase_path_segment(segment)
      URI.encode_www_form_component(URI.decode_www_form_component(segment).downcase)
    end

    def path(uri)
      uri_items(uri).first
    end

    def querystring(uri)
      uri_items(uri).last
    end

    def has_querystring?(uri)
      uri_items(uri).length > 1
    end

    def uri_items(uri)
      uri.split("?", 2)
    end

    def redirect_header(uri)
      [301, {"Location" => uri.to_s, "Content-Type" => "text/html"}, []]
    end
  end
end