| blib/lib/PAGI/Session.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 48 | 48 | 100.0 |
| branch | 20 | 20 | 100.0 |
| condition | 7 | 9 | 77.7 |
| subroutine | 14 | 14 | 100.0 |
| pod | 11 | 11 | 100.0 |
| total | 100 | 102 | 98.0 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package PAGI::Session; | ||||||
| 2 | |||||||
| 3 | 1 | 1 | 158395 | use strict; | |||
| 1 | 2 | ||||||
| 1 | 60 | ||||||
| 4 | 1 | 1 | 9 | use warnings; | |||
| 1 | 2 | ||||||
| 1 | 38 | ||||||
| 5 | 1 | 1 | 4 | use Scalar::Util 'blessed'; | |||
| 1 | 1 | ||||||
| 1 | 589 | ||||||
| 6 | |||||||
| 7 | =head1 NAME | ||||||
| 8 | |||||||
| 9 | PAGI::Session - Standalone helper object for session data access | ||||||
| 10 | |||||||
| 11 | =head1 SYNOPSIS | ||||||
| 12 | |||||||
| 13 | use PAGI::Session; | ||||||
| 14 | |||||||
| 15 | # Construct from raw session data, scope, or request object | ||||||
| 16 | my $session = PAGI::Session->new($scope->{'pagi.session'}); | ||||||
| 17 | my $session = PAGI::Session->new($scope); | ||||||
| 18 | my $session = PAGI::Session->new($req); # any object with ->scope | ||||||
| 19 | |||||||
| 20 | # Strict get - dies if key doesn't exist (catches typos) | ||||||
| 21 | my $user_id = $session->get('user_id'); | ||||||
| 22 | |||||||
| 23 | # Safe get with default for optional keys | ||||||
| 24 | my $theme = $session->get('theme', 'light'); | ||||||
| 25 | |||||||
| 26 | # Set, delete, check existence | ||||||
| 27 | $session->set('cart_count', 3); | ||||||
| 28 | $session->delete('cart_count'); | ||||||
| 29 | if ($session->exists('user_id')) { ... } | ||||||
| 30 | |||||||
| 31 | # List user keys (excludes internal _prefixed keys) | ||||||
| 32 | my @keys = $session->keys; | ||||||
| 33 | |||||||
| 34 | # Session lifecycle | ||||||
| 35 | $session->regenerate; # Request new session ID | ||||||
| 36 | $session->destroy; # Mark session for deletion | ||||||
| 37 | |||||||
| 38 | =head1 DESCRIPTION | ||||||
| 39 | |||||||
| 40 | PAGI::Session wraps the raw session data hashref and provides a clean | ||||||
| 41 | accessor interface with strict key checking. It is a standalone helper | ||||||
| 42 | that is not attached to any request or protocol object. | ||||||
| 43 | |||||||
| 44 | The strict C |
||||||
| 45 | typos at runtime. Use the two-argument form C |
||||||
| 46 | for keys that may or may not be present. | ||||||
| 47 | |||||||
| 48 | =head1 CONSTRUCTOR | ||||||
| 49 | |||||||
| 50 | =head2 new | ||||||
| 51 | |||||||
| 52 | my $session = PAGI::Session->new($data_hashref); | ||||||
| 53 | my $session = PAGI::Session->new($scope); | ||||||
| 54 | my $session = PAGI::Session->new($request); | ||||||
| 55 | |||||||
| 56 | Accepts raw session data (hashref), a PAGI scope (hashref with | ||||||
| 57 | C |
||||||
| 58 | (e.g., L |
||||||
| 59 | underlying hash, so mutations via C |
||||||
| 60 | visible to the session middleware. | ||||||
| 61 | |||||||
| 62 | =cut | ||||||
| 63 | |||||||
| 64 | sub new { | ||||||
| 65 | 28 | 28 | 1 | 186589 | my ($class, $arg) = @_; | ||
| 66 | |||||||
| 67 | 28 | 33 | my $data; | ||||
| 68 | 28 | 100 | 66 | 136 | if (blessed($arg) && $arg->can('scope')) { | ||
| 100 | 66 | ||||||
| 100 | |||||||
| 69 | # Duck-typed object with scope method (e.g., PAGI::Request, PAGI::SSE) | ||||||
| 70 | 1 | 4 | $data = $arg->scope->{'pagi.session'}; | ||||
| 71 | } | ||||||
| 72 | elsif (ref $arg eq 'HASH' && exists $arg->{'pagi.session'}) { | ||||||
| 73 | # Scope hashref | ||||||
| 74 | 1 | 3 | $data = $arg->{'pagi.session'}; | ||||
| 75 | } | ||||||
| 76 | elsif (ref $arg eq 'HASH') { | ||||||
| 77 | # Raw session data hashref | ||||||
| 78 | 22 | 27 | $data = $arg; | ||||
| 79 | } | ||||||
| 80 | |||||||
| 81 | 28 | 100 | 53 | die "PAGI::Session requires session data (hashref, scope, or object with ->scope)\n" | |||
| 82 | unless ref $data eq 'HASH'; | ||||||
| 83 | |||||||
| 84 | 24 | 61 | return bless { _data => $data }, $class; | ||||
| 85 | } | ||||||
| 86 | |||||||
| 87 | =head1 METHODS | ||||||
| 88 | |||||||
| 89 | =head2 id | ||||||
| 90 | |||||||
| 91 | my $id = $session->id; | ||||||
| 92 | |||||||
| 93 | Returns the session ID from C<< $data->{_id} >>. | ||||||
| 94 | |||||||
| 95 | =cut | ||||||
| 96 | |||||||
| 97 | sub id { | ||||||
| 98 | 4 | 4 | 1 | 12 | my ($self) = @_; | ||
| 99 | 4 | 17 | return $self->{_data}{_id}; | ||||
| 100 | } | ||||||
| 101 | |||||||
| 102 | =head2 get | ||||||
| 103 | |||||||
| 104 | my $value = $session->get('key'); # dies if missing | ||||||
| 105 | my $value = $session->get('key', $default); # returns $default if missing | ||||||
| 106 | |||||||
| 107 | Retrieves a value from the session. With one argument, dies with an | ||||||
| 108 | error including the key name if the key does not exist. With a default | ||||||
| 109 | argument, returns the default when the key is missing (even if the | ||||||
| 110 | default is C |
||||||
| 111 | |||||||
| 112 | =cut | ||||||
| 113 | |||||||
| 114 | sub get { | ||||||
| 115 | 12 | 12 | 1 | 58 | my ($self, $key, @rest) = @_; | ||
| 116 | 12 | 100 | 23 | if (!exists $self->{_data}{$key}) { | |||
| 117 | 5 | 100 | 13 | return $rest[0] if @rest; | |||
| 118 | 2 | 12 | die "No session key '$key'\n"; | ||||
| 119 | } | ||||||
| 120 | 7 | 25 | return $self->{_data}{$key}; | ||||
| 121 | } | ||||||
| 122 | |||||||
| 123 | =head2 set | ||||||
| 124 | |||||||
| 125 | $session->set('key', $value); | ||||||
| 126 | $session->set(user_id => 42, role => 'admin', email => 'john@example.com'); | ||||||
| 127 | |||||||
| 128 | Sets one or more keys in the session data. With two arguments, sets a | ||||||
| 129 | single key. With more arguments, treats them as key-value pairs. | ||||||
| 130 | Dies if given an odd number of arguments greater than one. | ||||||
| 131 | |||||||
| 132 | =cut | ||||||
| 133 | |||||||
| 134 | sub set { | ||||||
| 135 | 8 | 8 | 1 | 39 | my ($self, @args) = @_; | ||
| 136 | 8 | 100 | 100 | 28 | die "set() requires key => value pairs\n" if @args > 2 && @args % 2; | ||
| 137 | 7 | 100 | 10 | if (@args == 2) { | |||
| 138 | 6 | 19 | $self->{_data}{$args[0]} = $args[1]; | ||||
| 139 | } | ||||||
| 140 | else { | ||||||
| 141 | 1 | 3 | my %pairs = @args; | ||||
| 142 | 1 | 6 | $self->{_data}{$_} = $pairs{$_} for CORE::keys %pairs; | ||||
| 143 | } | ||||||
| 144 | } | ||||||
| 145 | |||||||
| 146 | =head2 exists | ||||||
| 147 | |||||||
| 148 | if ($session->exists('key')) { ... } | ||||||
| 149 | |||||||
| 150 | Returns true if the key exists in the session data. | ||||||
| 151 | |||||||
| 152 | =cut | ||||||
| 153 | |||||||
| 154 | sub exists { | ||||||
| 155 | 4 | 4 | 1 | 11 | my ($self, $key) = @_; | ||
| 156 | 4 | 100 | 16 | return exists $self->{_data}{$key} ? 1 : 0; | |||
| 157 | } | ||||||
| 158 | |||||||
| 159 | =head2 delete | ||||||
| 160 | |||||||
| 161 | $session->delete('key'); | ||||||
| 162 | $session->delete('k1', 'k2', 'k3'); | ||||||
| 163 | |||||||
| 164 | Removes one or more keys from the session data. | ||||||
| 165 | |||||||
| 166 | =cut | ||||||
| 167 | |||||||
| 168 | sub delete { | ||||||
| 169 | 2 | 2 | 1 | 6 | my ($self, @keys) = @_; | ||
| 170 | 2 | 8 | delete $self->{_data}{$_} for @keys; | ||||
| 171 | } | ||||||
| 172 | |||||||
| 173 | =head2 keys | ||||||
| 174 | |||||||
| 175 | my @keys = $session->keys; | ||||||
| 176 | |||||||
| 177 | Returns a list of user keys, filtering out internal keys that start | ||||||
| 178 | with an underscore (e.g. C<_id>, C<_created>, C<_last_access>). | ||||||
| 179 | |||||||
| 180 | =cut | ||||||
| 181 | |||||||
| 182 | sub keys { | ||||||
| 183 | 4 | 4 | 1 | 10 | my ($self) = @_; | ||
| 184 | 4 | 10 | return grep { !/^_/ } keys %{$self->{_data}}; | ||||
| 18 | 40 | ||||||
| 4 | 9 | ||||||
| 185 | } | ||||||
| 186 | |||||||
| 187 | =head2 slice | ||||||
| 188 | |||||||
| 189 | my %data = $session->slice('user_id', 'role', 'email'); | ||||||
| 190 | |||||||
| 191 | Returns a hash of key-value pairs for the requested keys. Keys that | ||||||
| 192 | do not exist in the session are silently skipped (unlike C |
||||||
| 193 | which dies on missing keys). | ||||||
| 194 | |||||||
| 195 | =cut | ||||||
| 196 | |||||||
| 197 | sub slice { | ||||||
| 198 | 3 | 3 | 1 | 15 | my ($self, @keys) = @_; | ||
| 199 | 3 | 100 | 5 | return map { CORE::exists($self->{_data}{$_}) ? ($_ => $self->{_data}{$_}) : () } @keys; | |||
| 7 | 20 | ||||||
| 200 | } | ||||||
| 201 | |||||||
| 202 | =head2 clear | ||||||
| 203 | |||||||
| 204 | $session->clear; | ||||||
| 205 | |||||||
| 206 | Removes all user keys from the session, preserving internal | ||||||
| 207 | C<_>-prefixed keys (C<_id>, C<_created>, C<_last_access>, etc.). | ||||||
| 208 | Use this for a "soft logout" that keeps the session ID but wipes | ||||||
| 209 | application data. | ||||||
| 210 | |||||||
| 211 | =cut | ||||||
| 212 | |||||||
| 213 | sub clear { | ||||||
| 214 | 2 | 2 | 1 | 8 | my ($self) = @_; | ||
| 215 | 2 | 3 | for my $key ($self->keys) { | ||||
| 216 | 3 | 5 | delete $self->{_data}{$key}; | ||||
| 217 | } | ||||||
| 218 | } | ||||||
| 219 | |||||||
| 220 | =head2 regenerate | ||||||
| 221 | |||||||
| 222 | $session->regenerate; | ||||||
| 223 | |||||||
| 224 | Requests session ID regeneration. The middleware will generate a new | ||||||
| 225 | session ID, delete the old session from the store, save session data | ||||||
| 226 | under the new ID, and update the client cookie/header. | ||||||
| 227 | |||||||
| 228 | B |
||||||
| 229 | session fixation attacks. | ||||||
| 230 | |||||||
| 231 | =cut | ||||||
| 232 | |||||||
| 233 | sub regenerate { | ||||||
| 234 | 1 | 1 | 1 | 4 | my ($self) = @_; | ||
| 235 | 1 | 3 | $self->{_data}{_regenerated} = 1; | ||||
| 236 | } | ||||||
| 237 | |||||||
| 238 | =head2 destroy | ||||||
| 239 | |||||||
| 240 | $session->destroy; | ||||||
| 241 | |||||||
| 242 | Marks the session for destruction. The middleware will delete the | ||||||
| 243 | session data from the store and clear the client-side state (e.g., | ||||||
| 244 | expire the cookie). Use this for logout. | ||||||
| 245 | |||||||
| 246 | =cut | ||||||
| 247 | |||||||
| 248 | sub destroy { | ||||||
| 249 | 1 | 1 | 1 | 6 | my ($self) = @_; | ||
| 250 | 1 | 4 | $self->{_data}{_destroyed} = 1; | ||||
| 251 | } | ||||||
| 252 | |||||||
| 253 | 1; | ||||||
| 254 | |||||||
| 255 | __END__ |