The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Dancer::Plugin::WindowSession;

use strict;
use warnings;
use Clone;
use Carp;

our $VERSION = '0.01';

use Dancer ':syntax';
use Dancer::Plugin;

sub _get_window_session_object
{
	my $win_sess_id ; ## The window session ID, from the HTTP request.
	my $win_sess_obj ; ## The Dancer::Session::Abstract object

	## Is there a cached version of the window-session object ?
	if (defined(var 'window_session_object')) {
		$win_sess_obj = var 'window_session_object';
		$win_sess_id = $win_sess_obj->id;
		debug "Retrieving Existing Window-Session-ID from 'vars': $win_sess_id\n";

		return $win_sess_obj;
	}


	# This is the first time 'window_session' is request during this request handling,
	# check if we have one in the HTTP Request
	$win_sess_id = param 'winsid';
	if ($win_sess_id) {
		debug "Retrieving Existing Window-Session-ID from HTTP-Request: $win_sess_id\n";
	} else {
		debug "No Window-Session-ID found\n";
	}

	my $session_engine = engine 'session'
		or die "Can't find session engine";

	# If we have a Window-session-Id, try to retrieve the corresponding hash.
	if ($win_sess_id) {
		debug "Retieving existing Window-Session for ID $win_sess_id\n";
		$win_sess_obj = $session_engine->retrieve($win_sess_id);

		## If we failed to retrieve existing window-session, force a new one
		$win_sess_id = undef unless $win_sess_obj;
	}

	# If anything failed along the way, just create a new window session
	if (!$win_sess_id) {
		debug "Generating new Window-Session object" ;
		$win_sess_obj = $session_engine->create();
		$win_sess_id = $win_sess_obj->id;
		debug "New Window-Session-ID: $win_sess_id\n";

	}
	## Cache the new window-session object,
	## for other route handlers downstream in the current request handling.
	var 'window_session_object' => $win_sess_obj;

	return $win_sess_obj;
}

hook before => sub {
	debug "Before-Hook: Getting Window-Session";
	my $window_session = _get_window_session_object;
	my $window_session_id = $window_session->id;
};

hook before_template_render => sub {
	my $tokens = shift ;

	my $window_session = _get_window_session_object;

	$tokens->{winsid} = $window_session->id;
	$tokens->{window_session} = Clone::clone($window_session);

	debug "Before-Template-Render-Hook: adding window-session-id: ". $window_session->id ;
};

hook after => sub {
	my $window_session = _get_window_session_object;
	my $window_session_id = $window_session->id;
	debug "After-Hook: Saving Window-Session: $window_session_id";
	$window_session->flush();
};

register window_session_id => sub {
	my $window_session = _get_window_session_object;
	return $window_session->id;
};


register window_session => sub {
	my $window_session = _get_window_session_object;
	my $window_session_id = $window_session->id;

	my ($key,$value) = @_;
	$key eq 'id' and croak 'Can\'t store to window_session key with name "id"';

	if (defined $value) {
		debug "Window-Session ($window_session_id), setting $key => $value";
		## Writ operation, as in:
		##  window_session 'varname' => 'new_value';
		$window_session->{$key} = $value;
	} else {
		## read operation, as in:
		##   my $data = window_session 'varname';
		$value = $window_session->{$key};
		my $valuestr = $value // "(undefined)";
		debug "Window-Session ($window_session_id), retrieving $key: returning $valuestr";
		return $value if defined $value;
	}
	return ;
};

register_plugin;

# ABSTRACT: A Dancer plugin for managing Browser Window Sessions

1;
__END__
=pod

=head1 NAME

Dancer::Plugin::WindowSession - Manage Per-Browser-Window sessions.

=head1 VERSION

version 0.01

=head1 SYNOPSIS

	use Dancer;
	use Dancer::Plugin::WindowSession;

	get '/' => sub {
		## Read Session-wide variable
		## (applies to all open browser windows)
		my $username = session 'username';

		## Read Window-Session variable
		## (will be different for every open browser window)
		my $color = window_session 'color';

		## [ return something to the user ]
	};


	## Assume the user submitted a POST <form>
	## with new data, save some variables to the standard session,
	## and others to the per-window session.
	post 'change_settings' => sub {
		my $username = param 'username';
		my $color = param 'color';

		session 'username' => $username ;
		window_session 'color' => $color ;

		## [ return something to the user ]
	};

	dance;

	######################
	### VERY IMPORTANT ###
	######################
	In all the template files, you must pass-on the 'winsid' CGI variable,
	either as part of a URL or as part of a POST <form> varaible.

	Using Template::Toolkit templtates:

	<a href="some_other_page?winsid=[% winsid | uri %]">Go to some other page</a>

	OR

	<form method="post">
		<input type="hidden" name="winsid" value="[% winsid|uri %]">
	</form>

=head1 FUNCTIONS

C<window_session> - Read/Write access to the per-window-session variables. Behaves exactly like L<Dancer>'s C<session> keyword.

C<window_session_id> - Returns the per-window-session ID number (if you need to embed it in a URL string).

=head1 DESCRIPTION

This module makes it easy to manage per-window session variables (as opposed to browser-wide session variables).

The common use case is when you expect users of your website to have multiple web-browser windows open with your web-site, and for each open window you want to maintain independant set of variables.


=head1 IMPLEMENTATION

To use this plugin effectively, be sure to include the C<winsid> value in B<all> URLs and POST forms you have in your templates.

This plugin uses the same session engine configured for your Dancer application (see L<Dancer::Session>).

=head1 CONFIGURATION

No configuration options are available, at the moment.

Future version might allow changing the name of the CGI varaible (C<winsid>) to something else.

=head1 AUTHOR

Assaf Gordon, C<< <gordon at cshl.edu> >>

=head1 BUGS

Possibly many.

B<NOTE>: If a user copies a URL (containing the C<winsid> value) and pastes it in a new browser window (or sends it to another user) - then both windows will share the same sessions. This can be viewed as a bug (The per-window mechanism does not really guarentee to be a single-window session) or a feature (users can easily share their session state with other users).

Please report any bugs or feature requests to
L<https://github.com/agordon/Dancer-Plugin-WindowSession/issues>

=head1 SEE ALSO

L<Dancer>, L<Dancer::Plugin>

=head1 Example

See working example at: L<http://winsid.cancan.cshl.edu> .

See the C<eg/> directory for a complete source of the example. Run: C<perl -I./lib/ eg/example/bin/app.pl> then visit L<http://localhost:3000> .

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Dancer::Plugin::WindowSession

=head1 ACKNOWLEDGEMENTS

The implementation was influenced by the UCSC Genome Browser website, which uses the C<hgsid> CGI variable in the same manner.

=head1 LICENSE AND COPYRIGHT

Copyright 2012 Assaf Gordon.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut