CGI-Plus

 view release on metacpan or  search on metacpan

lib/CGI/Plus.pm  view on Meta::CPAN

package CGI::Plus;
use strict;
use Carp;
use CGI::Safe 'taint';
use base 'CGI::Safe';
use String::Util ':all';
use CGI::Cookie;

# version
our $VERSION = '0.15';

# Debug::ShowStuff
# use Debug::ShowStuff ':all';
# use Debug::ShowStuff::ShowVar;

# enable file uploads
$CGI::DISABLE_UPLOADS = 0;

# maximum upload: 5 mb
$CGI::POST_MAX = 5 * 1024 * 1024;

# set path to empty string
$ENV{'PATH'} = '';

=head1 NAME

CGI::Plus -- Extra utilities for CGI

=head1 Description

This module adds a few enhancements to
L<CGI::Safe|http://search.cpan.org/~ovid/CGI-Safe/lib/CGI/Safe.pm>,
which itself adds a few security-based enancements to
L<CGI.pm|http://perldoc.perl.org/CGI.html>.  The enhancement are almost
entirely additions -  the only method that is overridden is new(), and
the changes there are only addition.  The enhancements in this module entirely
use the object-oriented interface.

=head1 SYNOPSIS

 use CGI::Plus;
 my ($cgi, $cookie, $url, $param);

 # new CGI::Plus object
 $cgi = CGI::Plus->new();

 # turn on checks for cross-site request forgeries (CSRF)
 $cgi->csrf(1);

 # get a cookie and look at its values
 $cookie = $cgi->incoming_cookies->{'mycookie'};
 print $cookie->{'values'}->{'x'}, "\n";
 print $cookie->{'values'}->{'y'}, "\n";

 # more concise way to get an incoming cookie
 $cookie = $cgi->ic->{'mycookie'};

 # resend a cookie, but change one of its values
 $cookie = $cgi->resend_cookie('mycookie');
 $cookie->{'values'}->{'x'} = 2;

 # add an outgoing cookie, set some values
 $cookie = $cgi->new_send_cookie('newcookie');
 $cookie->{'values'}->{'val1'} = '1';
 $cookie->{'values'}->{'val2'} = '2';

 # output HTTP header with outgoing cookies, including CSRF
 # check cookie, automatically added
 print $cgi->header_plus;
 
 # output header again if it hasn't already been sent, but if it
 # has then output an empty string
 print $cgi->header_plus;

 # output the URL of the current page but set a new value
 # for the "t" param and remove the "j" param
 $url = $cgi->self_link(params=>{t=>2, j=>undef});

 # check if the submitted form includes the value of the CSRF
 # cookie that was sent
 if (! $cgi->csrf_check)
     { die 'security error' }

 # output the randomly generated value of the CSRF cookie,
 # output: KTFnGgpkZ4
 print $cgi->csrf_value, "\n";

 # output the hidden input form field that uses the same
 # value as the CSRF cookie
 # output: <input type="hidden" name="csrf" value="KTFnGgpkZ4">
 print $cgi->csrf_field, "\n";

 # get the CSRF check param for use in a URL
 # output: csrf=KTFnGgpkZ4
 print $cgi->csrf_param;

 # set a custom header
 $cgi->set_header('myheader', 'whatever');

 # change content type
 $cgi->set_content_type('text/json');

 # output HTTP headers, including added cookies, the CSRF cookie,
 # and the new header
 print $cgi->header_plus;

 # outputs something like this:
 # Set-Cookie: newcookie=val2&2&val1&1; path=/
 # Set-Cookie: mycookie=y&2&x&2; path=/
 # Set-Cookie: csrf=v&KTFnGgpkZ4; path=/
 # Date: Sun, 29 Jul 2012 04:08:06 GMT
 # Myheader: whatever
 # Content-Type: text/json; charset=ISO-8859-1

=head1 INSTALLATION

CGI::Plus can be installed with the usual routine:

 perl Makefile.PL
 make
 make test
 make install

=head1 METHODS

=cut


#------------------------------------------------------------------------------
## new
#

=head2 CGI::Plus->new()

Creates and returns a CGI::Plus object.  New calls the super-class' new()
method, so all params sent to this method will be passed through to CGI
and CGI::Safe.

=cut

sub new {
	my $class = shift;
	my $cgi = $class->SUPER::new(@_);
	
	# set cookies
	$cgi->initialize_cookies();
	
	# call super method
	return $cgi;
}
#
# new
#------------------------------------------------------------------------------


#------------------------------------------------------------------------------
# ic, oc
# quick accesors to incoming and outgoing cookies
#

=head2 $cgi->ic, $cgi->oc


=cut

sub incoming_cookies { return $_[0]->{'cookies'}->{'incoming'} }
sub ic { return shift->incoming_cookies(@_) }

sub outgoing_cookies { return $_[0]->{'cookies'}->{'outgoing'} }
sub oc { return shift->outgoing_cookies(@_) }

#
# ic, oc
#------------------------------------------------------------------------------


#------------------------------------------------------------------------------
# initialize_cookies
# private method
#
sub initialize_cookies {
	my ($cgi) = @_;
	my ($got, %cookies);
	
	# cookie hashes
	$cgi->{'cookies'} = {};
	$got = $cgi->{'cookies'}->{'incoming'} = {};
	$cgi->{'cookies'}->{'outgoing'} = {};
	
	# get hash of cookies that were sent
	%cookies = CGI::Cookie->fetch();
	# showhash \%cookies, title=>'%cookies';
	
	# populate cookie values
	foreach my $name (keys %cookies) {
		my ($cookie, $element, @value);
		$cookie = $cookies{$name};
		$element = {};
		
		# name of cookie
		$element->{'name'} = $name;
		
		# original cookie object
		$element->{'org'} = $cookie;
		
		# expires
		if (defined $cookie->expires())
			{ $element->{'expires'} = $cookie->expires() }
		
		# get parsed values
		@value = $cookie->value();
		
		# if more than one element in @value, assume it's a hash
		if (@value > 1)
			{ $element->{'values'} = {@value} }
		
		# else it's a single string value
		else
			{ $element->{'value'} = $cookie->value() }
		
		# hold on to cookie element
		$got->{$name} = $element;
	}
}
#
# initialize_cookies
#------------------------------------------------------------------------------


#------------------------------------------------------------------------------
# cookie_resend
#
sub resend_cookie {
	my $cgi = shift;
	return $cgi->cookie_resend(@_);
}

sub cookie_resend {
	my ($cgi, $name, %opts) = @_;
	my ($got, $send);
	
	# default %opts
	%opts = (ensure=>1, %opts);
	
	# if not ensuring existence of cookie, and cookie doesn't
	# exist, return  undef
	unless ( $cgi->ic->{$name} || $opts{'ensure'}) {
		return undef;
	}
	
	# get sent cookie
	$got = $cgi->ic->{$name} || {'name'=>$name};
	
	# create cookie that gets sent back out
	$send = {};
	
	# clone $got cookie
	foreach my $key (keys %$got) {
		my $value = $got->{$key};
		
		# original cookie
		if (UNIVERSAL::isa $value, 'CGI::Cookie') {
			$send->{$key} = $value;
		}
		
		# hashref
		elsif (UNIVERSAL::isa $value, 'HASH') {
			$send->{$key} = {%$value};
		}
		
		# else just copy
		else {
			$send->{$key} = $value;
		}
	}
	
	# set cookie
	$cgi->oc->{$name} = $send;
	
	# return new cookie
	return $send;
}
#
# cookie_resend
#------------------------------------------------------------------------------


#------------------------------------------------------------------------------
# new_send_cookie
#
sub new_send_cookie {
	my ($cgi, $name) = @_;
	my ($cookie);
	
	# create oject
	$cookie = {};
	$cookie->{'name'} = $name;
	$cookie->{'values'} = {};
	
	# add to hash of outgoing cookies
	$cgi->oc->{$name} = $cookie;
	
	# return new cookie
	return $cookie;
}
#
# new_send_cookie
#------------------------------------------------------------------------------


#------------------------------------------------------------------------------
# self_link
#

=head2 $cgi->self_link(%options)

Returns a url that is a relative link to the current page.  The local path of
the URL is sent, but not the protocol or host.  So, for example, if the URL
of the current page is

 http://www.example.com/cgi-plus/?y=1&x=2&t=2&y=2

lib/CGI/Plus.pm  view on Meta::CPAN

	# return empty string
	if ($cgi->{'header_sent'})
		{ return '' }
	
	# set content type
	if ( (! $opts{'-type'}) && $cgi->{'content_type'} ) {
		$opts{'-type'} = $cgi->{'content_type'};
	}
	
	# add headers
	if ($cgi->{'headers'}) {
		while ( my($name, $value) = each(%{$cgi->{'headers'}}) ) {
			$name =~ s|^\-*|-|s;
			$opts{$name} ||= $value;
		}
	}
	
	# add cookies
	foreach my $name ( keys %{$cgi->oc} ) {
		my (%element, %params, $cookie);
		%element = %{$cgi->oc->{$name}};
		
		# 'values' takes precedence over 'value'
		if ($element{'values'})
			{ delete $element{'value'} }
		
		# else if no 'value' either, set it to empty string
		elsif (! defined $element{'value'})
			{ $element{'value'} = '' }
		
		# loop through values
		foreach my $key (keys %element) {
			# special case: values
			if ($key eq 'values') {
				$params{'-value'} = $element{$key};
			}
			
			# else just copy value
			else {
				my $send_key = $key;
				$send_key =~ s|^\-*|-|;
				
				$params{$send_key} = $element{$key};
			}
		}
		
		# set domain
		if ($element{'domain'})
			{ $params{'-domain'} = $element{'domain'} }
		
		# set expires: default to one year
		if (exists $element{'expires'}) {
			if (defined $element{'expires'})
				{ $params{'-expires'} = $element{'expires'} }
		}
		else {
			$params{'-expires'} = '+1y';
		}
		
		# create cookie object
		$cookie = CGI::Cookie->new(%params);
		
		if (! defined $cookie) {
			# showhash \%params, title=>'error generating cookie';
			die 'cookie error';
		}
		
		# add to cookie array
		push @cookies, $cookie;
	}
	
	# add cookies to header options
	if (@cookies)
		{ $opts{'-cookie'} = \@cookies }
	
	# note that header has been sent
	$cgi->{'header_sent'} = 1;
	
	# call super method
	return $cgi->SUPER::header(%opts);
}
#
# header_plus
#------------------------------------------------------------------------------




#------------------------------------------------------------------------------
# CSRF info
#

=head1 Cross-site request forgery (CSRF) defenses

A L<Cross-site request forgery|http://en.wikipedia.org/wiki/Cross-site_request_forgery>
(CSRF) is a technique for breaching a web site's security.  CSRF is one of the
most common web-site vulnerabilities.  CGI::Plus provides a technique for
protecting

=cut

#
# CSRF info
#------------------------------------------------------------------------------




#------------------------------------------------------------------------------
# csrf
#
sub csrf {
	my $cgi = shift;
	
	# set csrf value if sent
	if (@_) {
		$cgi->{'csrf'} = $_[0];
		
		# set csrf cookie
		if ($cgi->{'csrf'}) {
			my ($cookie);



( run in 0.789 second using v1.01-cache-2.11-cpan-22024b96cdf )