package Mojolicious::Plugin::DeCSRF;

use Mojo::Base 'Mojolicious::Plugin';

our $VERSION = '0.93';

sub register {
	my ($self, $app, $conf) = @_;

	my $base = Mojolicious::Plugin::DeCSRF::Base->new;
	$base->{token_length} = $conf->{token_length} if $conf->{token_length};
	$base->{token_name} = $conf->{token_name} if $conf->{token_name};
	$base->{on_mismatch} = $conf->{on_mismatch} if $conf->{on_mismatch};
	push @{$base->urls}, $conf->{urls} if $conf->{urls};

	$app->helper(decsrf => sub { $base->c(shift) });

	$app->hook(before_dispatch => sub {
			my $c = shift;
			return $c if $base->c($c)->check;
			if ($base->on_mismatch && ref($base->on_mismatch) eq 'CODE') {
				$base->on_mismatch->($c);
			} else {
				$c->render(
					text => "Forbidden!",
					status => 403,
				);
			}
		}
	);
}

package Mojolicious::Plugin::DeCSRF::Base;

use Mojo::Base -base;

my $_token_checked = 1;
has c => undef;
has token_length => 4;
has token_name => 'token';
has on_mismatch => undef;
has urls => sub { [] };

sub check {
	my $self = shift;
	my $c = $self->c;
	my $token = $c->session($self->token_name);
	$_token_checked = 1;
	if ($self->_match($c->req->url)) 
	{
		return 0 unless (
				$c->req->param($self->token_name) 
				&& $c->req->param($self->token_name) eq $token
			);
	};
	return 1;
}

sub url {
	my $self = shift;
	my $url = shift // '';
	my $c = $self->c;
	if ($_token_checked) {
		$c->session($self->token_name => $self->_token);
		$_token_checked = 0;
	}
	if ($self->_match($url)) {
		return $c->url_for($url)->query([$self->token_name => $c->session($self->token_name)]);
	}
	return $c->url_for($url);
}

sub _match {
	my ($self, $url) = @_;
	if (@{$self->urls} && $url) {
		foreach (@{$self->urls}) {
			return 1 if $self->c->url_for($url)->path =~ m;^$_$;;
		}
	}
	return 0;
}

sub _token {
	my @chars = ( "A" .. "Z", "a" .. "z", 0 .. 9, qw(@ $ - _) );
	return join("", @chars[ map { rand @chars } ( 1 .. shift->token_length ) ]);
}

1;

__END__

=head1 NAME

Mojolicious::Plugin::DeCSRF - Defend from CSRF attacks centrally.

=head1 SYNOPSIS

  # Mojolicious::Lite
  #!/usr/bin/env perl
  
  use Mojolicious::Lite;

  plugin 'DeCSRF' => {
    on_mismatch => sub {
      shift->render(template => '503', status => 503);
    },
    token_length => 8,
    token_name => 'csrf',
    urls => qw~/protected~
  };
  
  get '/' => sub {
    my $self = shift;
  } => 'index';
  
  get '/protected' => sub {
    my $self = shift;
  } => 'protected';

  app->start();
  
  __DATA__
  @@ layouts/default.html.ep
  <html>
    <body><%= content %></body>
  </html>
  @@ protected.html.ep
  % layout 'default';
  <a href="<%= decsrf->url('index') %>">Home</a>
  @@ index.html.ep
  % layout 'default';
  <a href="<%= decsrf->url('protected') %>">Protected</a>
  @@ 503.html.ep
  Service error!

=head1 DESCRIPTION

L<Mojolicious::Plugin::DeCSRF> is a L<Mojolicious> plugin that defend the framework from CSRF attacks centrally. With "good" strategy you have flexible control of the urls. "Good" strategy is wrap all of the urls with decsrf->url(URL) and control all urls that must be protected at one place with decsrf->urls(). 

=head1 OPTIONS

Options can change at any time.

=head2 C<< decsrf->on_mismatch >>

Set custom mismatch handling callback. Default is $self->render( text => "Forbidden!", status => 403);     

  decsrf->on_mismatch( sub {
    shift->render(template => '503', status => 503);
  } );

=head2 C<< decsrf->token_length >>

Set custom token length. Default length is 4 symbols from 'A-Z', 'a-z', '0-9', '@', '$', '-', '_' ranges.

  decsrf->token_length(40);

=head2 C<< decsrf->token_name >>

Set custom token name in url and session parameters. Default name is 'token'.

  decsrf->token_name('csrf');

=head2 C<< decsrf->urls >>

Set urls that must be protected. perlre can used.

  decsrf->urls(qw~/protected /.*?ected~);

=head1 METHODS

L<Mojolicious::Plugin::DeCSRF> inherits all methods from
L<Mojolicious::Plugin> and implements the following new ones.

=head2 C<register>

  $plugin->register();

Register plugin in L<Mojolicious> application.

=head2 C<< decsrf->url >>

Add 'token' param to url that match with decsrf->urls.

  #/protected?token=XXXX
  decsrf->url('/protected');
  
  #/protected?foo=bar&token=XXXX
  decsrf->url('/protected?foo=bar');
  
=head1 AUTHOR

Ilya Tokarev <sysadm@cpan.org>

=head1 COPYRIGHT AND LICENSE 

Copyright (C) 2013, Ilya Tokarev.

This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.

=head1 SEE ALSO

L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.

=cut
