class Kiss class Request _attr_accessor :controller # Data pertaining to the current request. _attr_reader :env, :protocol, :host, :request, :response, :exception_cache, :exception_email_sent, :path # Processes and responds to a request. # Returns array of response code, headers, and body. def initialize(env, controller, passed_config = {}) @_controller = controller @_config = passed_config @_files_cached_this_request = {} end def call(env) @_env = env if @_controller.rack_file && ( (env["PATH_INFO"] == '/favicon.ico') || (env["PATH_INFO"].sub!(/\A#{@_controller.asset_uri}/, '')) ) return @_controller.rack_file.call(env) elsif env["PATH_INFO"] =~ /favicon\.\w{3}\Z/ return [404, {'Content-type' => 'text/html'}, 'File not found'] end @_request = Rack::Request.new(env) @_protocol, @_app_host = (@_request.server.split(/\:\/\//, 2) rescue ['', '']) @_app_host = @_config[:app_host] if @_config[:app_host] @_app_uri = @_config[:app_uri] || @_request.script_name || '' @_host ||= @_request.host rescue '' # unfreeze path @_path = "#{@_request.path_info}" || '/' # remove extra path noise +[..][R]+GET... @_path.sub!(/\++(\[.*?\]\+*)*\[R\].*/, '') @_params = @_request.params @_query_string = @_request.query_string # catch and report exceptions in this block status_code, headers, body = handle_request(@_path, @_params) if body.respond_to?(:prepend_html) unless @_debug_messages.empty? extend Kiss::Debug body = prepend_debug(body) headers['Content-Type'] = 'text/html' headers['Content-Length'] = body.content_length.to_s end unless @_benchmarks.empty? stop_benchmark extend Kiss::Bench body = prepend_benchmarks(body) headers['Content-Type'] = 'text/html' headers['Content-Length'] = body.content_length.to_s end end return_database if @_db [status_code, headers, body] end def path_with_query_string @_path + (@_query_string.empty? ? '' : '?' + @_query_string) end def handle_request(path, params = {}) begin @_exception_cache = {} @_debug_messages = [] @_benchmarks = [] @_response = Rack::Response.new catch :kiss_request_done do action = invoke_action(path, params) extension = action.extension options = action.output_options if content_type = options[:content_type] || (extension ? Kiss.mime_type(extension) : nil) @_response['Content-Type'] = "#{content_type}; #{options[:document_encoding] || 'utf-8'}" end send_response(action.output, options) end finalize_session if @_session @_response.finish rescue StandardError, LoadError, SyntaxError => e handle_exception(e) end end def lookup_exception_handler(e) klass = e.class while klass break if controller.exception_handlers[klass] klass = klass.superclass end klass && controller.exception_handlers[klass] end def load_exception_handler(exception_handler); end def handle_exception(e) @_exception_messages = [] report = Kiss::ExceptionReport.generate(e, @_env, @_exception_cache, @_db ? @_db.last_query : nil) exception_message = e.message.sub(/\n.*/m, '') if @_controller.exception_log_file @_controller.exception_log_file.print(report + "\n--- End of exception report --- \n\n") end status_code = 500 body = report result = [status_code, { "Content-Type" => "text/html", "Content-Length" => body.content_length.to_s, "X-Kiss-Error-Type" => e.class.name, "X-Kiss-Error-Message" => exception_message }, body] should_send_exception_email = true unless @_loading_exception_handler @_loading_exception_handler = true begin exception_handler = lookup_exception_handler(e) if exception_handler should_send_exception_email = exception_handler[:send_email] new_result = load_exception_handler(exception_handler) if exception_handler[:action] result = handle_request(exception_handler[:action]) result[0] = exception_handler[:status_code] || 500 end end rescue StandardError, LoadError, SyntaxError => e result = handle_exception(e) end end if should_send_exception_email && !@_controller.exception_mailer_config.empty? app_name = @_controller.exception_mailer_config.app_name || @_controller.exception_mailer_config.app || 'Kiss' email_message = <<-EOT Subject: #{app_name} - #{e.class.name}#{exception_message.blank? ? '' : ": #{exception_message}"} Content-type: text/html #{report} EOT send_email(@_controller.exception_mailer_config.merge(:message => email_message)) @_exception_email_sent = true end result end ##### ACTION METHODS ##### def invoke_action(path, params = {}, render_options = {}) action = get_action(path, params) catch :kiss_action_done do action.authenticate if action.class.authentication_required action.before_call action.call action.render(render_options) end action.after_call action end # Parses request URI to determine action path and arguments, then # instantiates action class to create action handler. def get_action(path, params) @@action_class ||= Kiss::Action.get_root_class(@_controller, @_controller.action_dir) # return action handler (instance of action class) @@action_class.get_subclass_from_path(path, self, params) end ##### FILE METHODS ##### # If file has already been cached in handling the current request, retrieve from cache # and do not check filesystem for updates. Else cache file via controller's file_cache. def file_cache(path, *args, &block) return @_controller.file_cache[path] if @_files_cached_this_request[path] @_files_cached_this_request[path] = true @_controller.file_cache(path, *args, &block) end ##### DATABASE METHODS ##### # Acquires and returns a database connection object from the connection pool. # # Tip: `db' is a shorthand alias for `database'. def database @_db ||= begin db = @_controller.database check_evolution_number(db) db.kiss_request = self db end end alias_method :db, :database def return_database @_db.kiss_request = nil @_controller.return_database(@_db) end # Kiss Model cache, used to invoke and store Kiss database models. # # Example: # models[:users] == database model for `users' table # # Tip: `dbm' (stands for `database models') is a shorthand alias for `models'. def models # make sure we have a database connection # create new model cache unless exists already db.kiss_model_cache end alias_method :dbm, :models # Check whether there exists a file in evolution_dir whose number is greater than app's # current evolution number. If so, raise an error to indicate need to apply new evolutions. def check_evolution_number(db) db_version = db.evolution_number if @_controller.directory_exists?(@_controller.evolution_dir) && @_controller.evolution_file(db_version+1) raise <<-EOT database evolution number #{db_version} < last evolution file number #{@_controller.last_evolution_file_number} apply evolutions or set database evolution number EOT end end ##### SESSION METHODS ##### # Retrieves or generates session data object, based on session ID from cookie value. def session @_session ||= begin @_controller.session_class ? begin @_controller.session_setup ||= begin # setup session storage @_controller.session_class.setup_storage(self) true end session = @_controller.session_class.persist(self, @_request.cookies[@_controller.cookie_name]) @_session_fingerprint = Marshal.dump(session.data).hash cookie_vars = { :value => session.values[:session_id], :path => @_config[:cookie_path] || @_app_uri, :domain => @_config[:cookie_domain] || @_request.host } cookie_vars[:expires] = Time.now + @_config[:cookie_lifespan] if @_config[:cookie_lifespan] # set_cookie here or at render time @_response.set_cookie @_controller.cookie_name, cookie_vars session end : {} end end # Saves session to session store, if session data has changed since load. def finalize_session @_session.save if @_session_fingerprint != Marshal.dump(@_session.data).hash end # Returns a Kiss::Login object containing data from session.login. def login @_login ||= Kiss::Login.new(session) end ##### OUTPUT METHODS ##### # Outputs a Kiss::StaticFile object as response to Rack. # Used to return static files efficiently. def send_file(path, options = {}) @_response = Kiss::StaticFile.new(path, options) throw :kiss_request_done end # Prepares Rack::Response object to return application response to Rack. def send_response(output = '', options = {}) @_response['Content-Length'] = output.content_length.to_s @_response['Content-Type'] = options[:content_type] if options[:content_type] if options[:filename] @_response['Content-Disposition'] = "#{options[:disposition] || 'inline'}; filename=#{options[:filename]}" elsif options[:disposition] @_response['Content-Disposition'] = options[:disposition] end @_response.body = output throw :kiss_request_done end # Sends HTTP 302 response to redirect client browser agent to specified URL. def redirect_url(url) @_response.status = 302 @_response['Location'] = url @_response.body = '' throw :kiss_request_done end # Redirects to specified action path, which may also include arguments. def redirect_action(action, options = {}) redirect_url( app_url(options) + action + (options[:params] ? '?' + options[:params].keys.map do |k| "#{k.to_s.url_escape}=#{options[:params][k].to_s.url_escape}" end.join('&') : '') ) end ##### DEBUG/BENCH OUTPUT ##### # Adds debug message to inspect object. Debug messages will be shown at top of # application response body. def debug(object, context = Kernel.caller[0]) @_debug_messages.push( [object.inspect.gsub(/ /, '  '), context] ) object end alias_method :trace, :debug # Starts a new benchmark timer, with optional label. Benchmark results will be shown # at top of application response body. def start_benchmark(label = nil, context = Kernel.caller[0]) stop_benchmark(context) @_benchmarks.push( :label => label, :start_time => Time.now, :start_context => context ) end alias_method :bench, :start_benchmark # Stops last benchmark timer, if still running. def stop_benchmark(end_context = nil) if @_benchmarks[-1] && !@_benchmarks[-1][:end_time] @_benchmarks[-1][:end_time] = Time.now @_benchmarks[-1][:end_context] = end_context end end alias_method :bench_stop, :stop_benchmark ##### OTHER METHODS ##### # Returns URL/URI of app root (corresponding to top level of action_dir). def app_url(options = {}) @_controller.app_url({ :protocol => @_protocol, :host => @_app_host, :uri => @_app_uri }.merge(options)) end # Returns URL/URI of app's static assets (asset_host or public_uri). def pub_url(options = {}) if options.empty? @_pub ||= (@_controller.asset_host ? @_protocol + '://' + @_controller.asset_host : '') + (options[:uri] || @_controller.asset_uri || '') else (options[:protocol] || @_protocol) + '://' + (options[:host] || @_controller.asset_host) + (options[:uri] || @_controller.asset_uri || '') end end alias_method :asset_url, :pub_url def query_string_with_params(params = {}) params = @_request.GET.merge(params) params.keys.map do |key| "#{key.to_s.url_escape}=#{params[key].to_s.url_escape}" end.join('&') end def url_with_params(params = {}) "#{app_url}#{@_path}?#{query_string_with_params(params)}" end # Adds data to be displayed in "Cache" section of Kiss exception reports. def exception_cache(data = nil) @_exception_cache.merge!(data) if data @_exception_cache end alias_method :set_exception_cache, :exception_cache # Returns new Kiss::Mailer object using specified options. def new_email(options = {}) controller.new_email({ :request => self }.merge(options)) end def send_email(options = {}) new_email(options).send end def cookies request.cookies end def set_cookie(*args) response.set_cookie(*args) end def path_info @_request.env["PATH_INFO"] end end end