# frozen_string_literal: true require "rack/session/abstract/id" module ActionDispatch class Request # Session is responsible for lazily loading the session from store. class Session # :nodoc: DisabledSessionError = Class.new(StandardError) ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc: ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc: # Singleton object used to determine if an optional param wasn't specified. Unspecified = Object.new # Creates a session hash, merging the properties of the previous session if any. def self.create(store, req, default_options) session_was = find req session = Request::Session.new(store, req) session.merge! session_was if session_was set(req, session) Options.set(req, Request::Session::Options.new(store, default_options)) session end def self.disabled(req) new(nil, req, enabled: false).tap do Session::Options.set(req, Session::Options.new(nil, { id: nil })) end end def self.find(req) req.get_header ENV_SESSION_KEY end def self.set(req, session) req.set_header ENV_SESSION_KEY, session end def self.delete(req) req.delete_header ENV_SESSION_KEY end class Options # :nodoc: def self.set(req, options) req.set_header ENV_SESSION_OPTIONS_KEY, options end def self.find(req) req.get_header ENV_SESSION_OPTIONS_KEY end def initialize(by, default_options) @by = by @delegate = default_options.dup end def [](key) @delegate[key] end def id(req) @delegate.fetch(:id) { @by.send(:extract_session_id, req) } end def []=(k, v); @delegate[k] = v; end def to_hash; @delegate.dup; end def values_at(*args); @delegate.values_at(*args); end end def initialize(by, req, enabled: true) @by = by @req = req @delegate = {} @loaded = false @exists = nil # We haven't checked yet. @enabled = enabled end def id options.id(@req) end def enabled? @enabled end def options Options.find @req end def destroy clear if enabled? options = self.options || {} @by.send(:delete_session, @req, options.id(@req), options) # Load the new sid to be written with the response. @loaded = false load_for_write! end end # Returns value of the key stored in the session or # +nil+ if the given key is not found in the session. def [](key) load_for_read! key = key.to_s if key == "session_id" id&.public_id else @delegate[key] end end # Returns the nested value specified by the sequence of keys, returning # +nil+ if any intermediate step is +nil+. def dig(*keys) load_for_read! keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key } @delegate.dig(*keys) end # Returns true if the session has the given key or false. def has_key?(key) load_for_read! @delegate.key?(key.to_s) end alias :key? :has_key? alias :include? :has_key? # Returns keys of the session as Array. def keys load_for_read! @delegate.keys end # Returns values of the session as Array. def values load_for_read! @delegate.values end # Writes given value to given key of the session. def []=(key, value) load_for_write! @delegate[key.to_s] = value end # Clears the session. def clear load_for_delete! @delegate.clear end # Returns the session as Hash. def to_hash load_for_read! @delegate.dup.delete_if { |_, v| v.nil? } end alias :to_h :to_hash # Updates the session with given Hash. # # session.to_hash # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"} # # session.update({ "foo" => "bar" }) # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"} # # session.to_hash # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"} def update(hash) load_for_write! @delegate.update hash.stringify_keys end # Deletes given key from the session. def delete(key) load_for_delete! @delegate.delete key.to_s end # Returns value of the given key from the session, or raises +KeyError+ # if can't find the given key and no default value is set. # Returns default value if specified. # # session.fetch(:foo) # # => KeyError: key not found: "foo" # # session.fetch(:foo, :bar) # # => :bar # # session.fetch(:foo) do # :bar # end # # => :bar def fetch(key, default = Unspecified, &block) load_for_read! if default == Unspecified @delegate.fetch(key.to_s, &block) else @delegate.fetch(key.to_s, default, &block) end end def inspect if loaded? super else "#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>" end end def exists? return false unless enabled? return @exists unless @exists.nil? @exists = @by.send(:session_exists?, @req) end def loaded? @loaded end def empty? load_for_read! @delegate.empty? end def merge!(other) load_for_write! @delegate.merge!(other) end def each(&block) to_hash.each(&block) end private def load_for_read! load! if !loaded? && exists? end def load_for_write! if enabled? load! unless loaded? else raise DisabledSessionError, "Your application has sessions disabled. To write to the session you must first configure a session store" end end def load_for_delete! load! if enabled? && !loaded? end def load! if enabled? id, session = @by.load_session @req options[:id] = id @delegate.replace(session.stringify_keys) end @loaded = true end end end end