lib/roda/session_middleware.rb
# frozen-string-literal: true require_relative '../roda' require_relative 'plugins/sessions' # Session middleware that can be used in any Rack application # that uses Roda's sessions plugin for encrypted and signed cookies. # See Roda::RodaPlugins::Sessions for details on options. class RodaSessionMiddleware # Class to hold session data. This is designed to mimic the API # of Rack::Session::Abstract::SessionHash, but is simpler and faster. # Undocumented methods operate the same as hash methods, but load the # session from the cookie if it hasn't been loaded yet, and convert # keys to strings. # # One difference between SessionHash and Rack::Session::Abstract::SessionHash # is that SessionHash does not attempt to setup a session id, since # one is not needed for cookie-based sessions, only for sessions # that are loaded out of a database. If you need to have a session id # for other reasons, manually create a session id using a randomly generated # string. class SessionHash # The Roda::RodaRequest subclass instance related to the session. attr_reader :req # The underlying data hash, or nil if the session has not yet been # loaded. attr_reader :data def initialize(req) @req = req end # The Roda sessions plugin options used by the middleware for this # session hash. def options @req.roda_class.opts[:sessions] end def each(&block) load! @data.each(&block) end def [](key) load! @data[key.to_s] end def fetch(key, default = (no_default = true), &block) load! if no_default @data.fetch(key.to_s, &block) else @data.fetch(key.to_s, default, &block) end end def has_key?(key) load! @data.has_key?(key.to_s) end alias :key? :has_key? alias :include? :has_key? def []=(key, value) load! @data[key.to_s] = value end alias :store :[]= # Clear the session, also removing a couple of roda session # keys from the environment so that the related cookie will # either be set or cleared in the rack response. def clear load! env = @req.env env.delete('roda.session.created_at') env.delete('roda.session.updated_at') @data.clear end alias :destroy :clear def to_hash load! @data.dup end def update(hash) load! hash.each do |key, value| @data[key.to_s] = value end @data end alias :merge! :update def replace(hash) load! @data.clear update(hash) end def delete(key) load! @data.delete(key.to_s) end # If the session hasn't been loaded, display that. def inspect if loaded? @data.inspect else "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>" end end # Return whether the session cookie already exists. # If this is false, then the session was set to an empty hash. def exists? load! req.env.has_key?('roda.session.serialized') end # Whether the session has already been loaded from the cookie yet. def loaded? !!defined?(@data) end def empty? load! @data.empty? end def keys load! @data.keys end def values load! @data.values end private # Load the session from the cookie. def load! @data ||= @req.send(:_load_session) end end module RequestMethods # Work around for if type_routing plugin is loaded into Roda class itself. def _remaining_path(_) end end # Setup the middleware, passing +opts+ as the Roda sessions plugin options. def initialize(app, opts) mid = Class.new(Roda) mid.plugin :sessions, opts @req_class = mid::RodaRequest @req_class.send(:include, RequestMethods) @app = app end # Initialize the session hash in the environment before calling the next # application, and if the session has been loaded after the result has been # returned, then persist the session in the cookie. def call(env) session = env['rack.session'] = SessionHash.new(@req_class.new(nil, env)) res = @app.call(env) if session.loaded? session.req.persist_session(res[1], session.data) end res end end