lib/middleman-core/core_extensions/request.rb



# Built on Rack
require "rack"
require "rack/file"
require "rack/lint"

module Middleman
  module CoreExtensions

    # Base helper to manipulate asset paths
    module Request

      # Extension registered
      class << self
        # @private
        def registered(app)

          # CSSPIE HTC File
          ::Rack::Mime::MIME_TYPES['.htc'] = 'text/x-component'

          # Let's serve all HTML as UTF-8
          ::Rack::Mime::MIME_TYPES['.html'] = 'text/html; charset=utf-8'
          ::Rack::Mime::MIME_TYPES['.htm'] = 'text/html; charset=utf-8'

          app.extend ClassMethods
          app.extend ServerMethods

          Middleman.extend CompatibleClassMethods

          # Include instance methods
          app.send :include, InstanceMethods
        end
        alias :included :registered
      end

      module ClassMethods
        # Reset Rack setup
        #
        # @private
        def reset!
          @rack_app = nil
        end

        # Get the static instance
        #
        # @private
        # @return [Middleman::Application]
        def inst(&block)
          @inst ||= begin
            mm = new(&block)
            mm.run_hook :ready
            mm
          end
        end

        # Set the shared instance
        #
        # @private
        # @param [Middleman::Application] inst
        # @return [void]
        def inst=(inst)
          @inst = inst
        end

        # Return built Rack app
        #
        # @private
        # @return [Rack::Builder]
        def to_rack_app(&block)
          @rack_app ||= begin
            app = ::Rack::Builder.new
            app.use Rack::Lint

            Array(@middleware).each do |klass, options, block|
              app.use(klass, *options, &block)
            end

            inner_app = inst(&block)
            app.map("/") { run inner_app }

            Array(@mappings).each do |path, block|
              app.map(path, &block)
            end

            app
          end
        end

        # Prototype app. Used in config.ru
        #
        # @private
        # @return [Rack::Builder]
        def prototype
          reset!
          to_rack_app
        end

        # Call prototype, use in config.ru
        #
        # @private
        def call(env)
          prototype.call(env)
        end

        # Use Rack middleware
        #
        # @param [Class] middleware Middleware module
        # @return [void]
        def use(middleware, *args, &block)
          @middleware ||= []
          @middleware << [middleware, args, block]
        end

        # Add Rack App mapped to specific path
        #
        # @param [String] map Path to map
        # @return [void]
        def map(map, &block)
          @mappings ||= []
          @mappings << [map, block]
        end
      end

      module ServerMethods
        # Create a new Class which is based on Middleman::Application
        # Used to create a safe sandbox into which extensions and
        # configuration can be included later without impacting
        # other classes and instances.
        #
        # @return [Class]
        def server(&block)
          @@servercounter ||= 0
          @@servercounter += 1
          const_set("MiddlemanApplication#{@@servercounter}", Class.new(Middleman::Application, &block))
        end
      end

      module CompatibleClassMethods
        # Create a new Class which is based on Middleman::Application
        # Used to create a safe sandbox into which extensions and
        # configuration can be included later without impacting
        # other classes and instances.
        #
        # @return [Class]
        def server(&block)
          ::Middleman::Application.server(&block)
        end
      end

      # Methods to be mixed-in to Middleman::Application
      module InstanceMethods
        # Backwards-compatibility with old request.path signature
        def request
          Thread.current[:legacy_request]
        end

        # Accessor for current path
        # @return [String]
        def current_path
          Thread.current[:current_path]
        end

        # Set the current path
        #
        # @param [String] path The new current path
        # @return [void]
        def current_path=(path)
          Thread.current[:current_path] = path
          Thread.current[:legacy_request] = ::Thor::CoreExt::HashWithIndifferentAccess.new({
            :path   => path,
            :params => req ? ::Thor::CoreExt::HashWithIndifferentAccess.new(req.params) : {}
          })
        end

        delegate :use, :to => :"self.class" 
        delegate :map, :to => :"self.class" 

        # Rack request
        # @return [Rack::Request]
        def req
          Thread.current[:req]
        end
        def req=(value)
          Thread.current[:req] = value
        end

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

        # Rack Interface
        #
        # @param env Rack environment
        def call!(env)
          # Store environment, request and response for later
          self.req = req = ::Rack::Request.new(env)
          res = ::Rack::Response.new

          logger.debug "== Request: #{env["PATH_INFO"]}"

          # Catch :halt exceptions and use that response if given
          catch(:halt) do
            process_request(env, req, res)

            res.status = 404

            res.finish
          end
        end

        # Halt the current request and return a response
        #
        # @param [String] response Response value
        def halt(response)
          throw :halt, response
        end

        # Core response method. We process the request, check with
        # the sitemap, and return the correct file, response or status
        # message.
        #
        # @param env
        # @param [Rack::Request] req
        # @param [Rack::Response] res
        def process_request(env, req, res)
          start_time = Time.now
          current_path = nil

          request_path = URI.decode(env["PATH_INFO"].dup)
          if request_path.respond_to? :force_encoding
            request_path.force_encoding('UTF-8')
          end
          request_path = full_path(request_path)

          # Run before callbacks
          run_hook :before

          # Get the resource object for this path
          resource = sitemap.find_resource_by_destination_path(request_path)

          # Return 404 if not in sitemap
          return not_found(res, request_path) unless resource && !resource.ignored?

          # If this path is a binary file, send it immediately
          return send_file(resource, env) if resource.binary?

          current_path = resource.destination_path

          res['Content-Type'] = resource.content_type || 'text/plain'

          begin
            # Write out the contents of the page
            output = resource.render do
              self.req = req
              self.current_path = current_path
            end

            res.write output
            # Valid content is a 200 status
            res.status = 200
          rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e
            res.write "Error: #{e.message}"
            res.status = 500
          end

          # End the request
          logger.debug "== Finishing Request: #{current_path} (#{(Time.now - start_time).round(2)}s)"
          halt res.finish
        end

        # Add a new mime-type for a specific extension
        #
        # @param [Symbol] type File extension
        # @param [String] value Mime type
        # @return [void]
        def mime_type(type, value)
          type = ".#{type}" unless type.to_s[0] == ?.
          ::Rack::Mime::MIME_TYPES[type] = value
        end

        # Halt request and return 404
        def not_found(res, path)
          res.status = 404
          res.write "<html><body><h1>File Not Found</h1><p>#{path}</p></body>"
          res.finish
        end

        # Immediately send static file
        #
        # @param [String] path File to send
        def send_file(resource, env)
          file      = ::Rack::File.new nil
          file.path = resource.source_file
          response = file.serving(env)
          status = response[0]
          response[1]['Content-Encoding'] = 'gzip' if %w(.svgz .gz).include?(resource.ext)
          # Do not set Content-Type if status is 1xx, 204, 205 or 304, otherwise
          # Rack will throw an error (500)
          if !(100..199).include?(status) && ![204, 205, 304].include?(status)
            response[1]['Content-Type'] = resource.content_type || "application/octet-stream"
          end
          halt response
        end
      end
    end
  end
end