package Mojolicious::Plugin::CSRFProtect;
use strict;
use warnings;
use Carp qw/croak/;

use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Util qw/md5_sum/;
use Mojo::ByteStream qw/b/;

our $VERSION = '0.07';

sub register {
    my ( $self, $app ) = @_;
    my $original_form_for = $app->renderer->helpers->{form_for};
    croak qq{Cannot find helper "form_for". Please, load plugin "TagHelpers" before}
        unless $original_form_for;

    # Replace "form_for" helper
    $app->helper(
        form_for => sub {
            my $c = shift;
            if ( defined $_[-1] && ref( $_[-1] ) eq 'CODE' ) {
                my $cb = $_[-1];
                $_[-1] = sub {
                    $app->hidden_field( 'csrftoken' => $self->_csrftoken($c) ) . $cb->();
                };
            }
            return $app->$original_form_for(@_);
        } );

    # Add "csrftoken" helper
    $app->helper( csrftoken => sub { $self->_csrftoken( $_[0] ) } );

    # Add "is_valid_csrftoken" helper
    $app->helper( is_valid_csrftoken => sub { $self->_is_valid_csrftoken( $_[0] ) } );

    # Add "jquery_ajax_csrf_protection" helper
    $app->helper(
        jquery_ajax_csrf_protection => sub {
            my $js = '<meta name="csrftoken" content="' . $self->_csrftoken( $_[0] ) . '"/>';
            $js .= q!<script type="text/javascript">!;
            $js .= q! $(document).ajaxSend(function(e, xhr, options) { !;
            $js .= q!    var token = $("meta[name='csrftoken']").attr("content");!;
            $js .= q! xhr.setRequestHeader("X-CSRF-Token", token);!;
            $js .= q! });</script>!;

            b($js);
        } );

    # input check
    $app->hook(
        after_static_dispatch => sub {
            my ($c) = @_;

            my $request_token = $c->req->param('csrftoken');
            #my $is_ajax = ( $c->req->headers->header('X-Requested-With') || '' ) eq 'XMLHttpRequest';

            if ( $c->req->method !~ m/^(?:GET|HEAD)$/ && !$self->_is_valid_csrftoken($c) ) {
                my $path = $c->tx->req->url->to_abs->to_string;
                $c->app->log->debug("CSRFProtect: Wrong CSRF protection token for [$path]!");

                $c->render(
                    status => 403,
                    text   => "Forbidden!",
                );

                return;
            }

            return 1;
        } );

}

sub _is_valid_csrftoken {
    my ( $self, $c ) = @_;

    my $valid_token = $c->session('csrftoken');
    my $form_token = $c->req->headers->header('X-CSRF-Token') || $c->param('csrftoken');
    unless ( $valid_token && $form_token && $form_token eq $valid_token ) {
        return 0;
    }

    return 1;
}

sub _csrftoken {
    my ( $self, $c ) = @_;

    return $c->session('csrftoken') if $c->session('csrftoken');

    my $token = md5_sum( md5_sum( time() . {} . rand() . $$ ) );

    $c->session( 'csrftoken' => $token );
    return $token;
}

1;

__END__

=head1 NAME

Mojolicious::Plugin::CSRFProtect - fully protects you from CSRF attacks

=head1 SYNOPSIS

  # Mojolicious
  $self->plugin('CSRFProtect');

  # Mojolicious::Lite
  plugin 'CSRFProtect';
  
  # Use "form_for" helper and all your html forms will have CSRF protection token 

    <%= form_for login => (method => 'post') => begin %>
           <%= text_field 'first_name' %>
           <%= submit_button %>
    <% end %>
    
  # Place jquery_ajax_csrf_protection helper to your layout template 
  # and all on GET/HEAD  AJAX requests will have CSRF protection token (requires JQuery)
   
    <%= jquery_ajax_csrf_protection %>


=head1 DESCRIPTION

L<Mojolicious::Plugin::CSRFProtect> is a L<Mojolicious> plugin which fully protects you from CSRF attacks.

It does next things:

1. Adds a hidden input (with name 'csrftoken') with CSRF protection token to every form 
(works only if you use C<form_for> helper from Mojolicious::Plugin::TagHelpers.) 

2. Adds the header "X-CSRF-Token" with CSRF token to every AJAX request (works with JQuery only)   

3. Rejects all non GET/HEAD requests without the correct CSRF protection token.
 

If you want protect your GET/HEAD requests then you can do it manually

In template: <a href="/delete_user/123/?csrftoken=<%= csrftoken %>">

In controller: $self->is_valid_csrftoken() 

=head1 HELPERS

=head2 C<form_for>

    This helper overrides the C<form_for> helper from Mojolicious::Plugin::TagHelpers 
    
    and adds hidden input with CSRF protection token.

=head2 C<jquery_ajax_csrf_protection>

    This helper adds CSRF protection headers to all JQuery AJAX requests.
    
    You should add <%= jquery_ajax_csrf_protection %> in head of your HTML page. 

=head2 C<csrftoken>

    returns  CSRF Protection token. 
    
    In templates <%= csrftoken %>
    
    In controller $self->csrftoken;
    
=head2 C<is_valid_csrftoken>

    With this helper you can check $csrftoken manually. It will take $csrftoken from $c->param('csrftoken');
     
    $self->is_valid_csrftoken() will return 1 or 0

=head1 AUTHOR

Viktor Turskyi <koorchik@cpan.org>

=head1 BUGS

Please report any bugs or feature requests to C<bug-mojolicious-plugin-csrfprotect at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Mojolicious-Plugin-CSRFProtect>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

Also you can report bugs to Github L<https://github.com/koorchik/Mojolicious-Plugin-CSRFProtect/>

=head1 SEE ALSO

=over 4

=item L<Mojolicious::Plugin::CSRFDefender>  

This plugin followes the same aproach but it works in different manner. 

It will parse your response body searching for '<form>' tag and then will insert CSRF token there.

=back

=head1 LICENSE AND COPYRIGHT

Copyright 2011 Viktor Turskyi

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
