Revision 3 (by ahitrov@rambler.ru, 2010/03/24 15:19:32) The CORE
package Utils::Spam::SecretForm;

use strict;
use Digest::MD5;
use Contenido::Globals;
use Scalar::Util qw(blessed);

# ��������� ������ ��� ������� � hidden-���� ����� 
# ��� ������������ �������� ������������.
#   extra - �������������� �������� ��� ��������� ������ 
#   (� �������, ���� ������� ���������, ����� ���������� ��� �����)
#   �� ������ ���������� ������ �������� ��� ���������.
sub generate {
    my %opts = @_;

    my ($start_time, $secret_code) = &get_secret_code(allow_generate_code => 1, memd => $opts{'memd'});
    return unless $start_time && $secret_code;

    my $random = &get_random_string(5);
    # start secret code time | generate time | hash
    return $start_time.'|'.time().'|'.$random.'|'.Digest::MD5::md5_hex($start_time.$secret_code.$random.$opts{'extra'});
}

# ��������� �������� hidden-���� �����
#   secret - ���, ��� ��������
#   ttl - ����� ����� ��������� ����
#   check_count - true �������� ���������� �� ������� ���������� �������������
sub validate {
    my %opts = @_;

    my $user_secret = $opts{'secret'};
    my $ttl         = $opts{'ttl'} || 3600;
    my $extra       = $opts{'extra'};
    my $check_count = $opts{'check_count'};
    my $result      = { is_valid => 1, is_expired => 0, count => 1 };
    my $memd        = blessed($opts{'memd'}) ? $opts{'memd'} : $keeper->MEMD();

    return $result unless blessed($memd);

    # ���������� �� ������������ ������
    my ($user_start_time, $user_generate_time, $user_random, $user_hash) = split(/\|/, $user_secret);

    # ������ � ��������� ������
    my ($start_time, $secret_code) = &get_secret_code(time => $user_start_time, memd => $memd);

    # ���� ��� �� �������� ��������� ����� ���������
    return $result if !defined($start_time) && !defined($secret_code);

    if (Digest::MD5::md5_hex($start_time.$secret_code.$user_random.$extra) ne $user_hash) {
        $result->{'is_valid'} = 0;
    }

    if ($result->{'is_valid'} && (($user_generate_time-$start_time > 3600) 
                                    || (time()-$user_generate_time > $ttl))
    ) {
        $result->{'is_expired'} = 1;
    }

    # ���� ���������� �������� �� ��������� �������������
    # ������ � ���������� ������������� ���������� ��������� � ����
    if ($result->{'is_valid'} && !$result->{'is_expired'} && $check_count) {

        $result->{'count'} = $memd->incr('usersecret|'.$user_secret, 1) if $memd;

        unless ($result->{'count'}) {
            $memd->add('usersecret|'.$user_secret, 1, $ttl) if $memd;
            $result->{'count'} = 1;
        }
    }

    return $result;
}

# ���������, ������� ���������� true ��� false � ����������� �� ����������, ��������������
#   � ���������� �������������
sub is_valid_secret {
    my %opts = @_;

    my $validate = &validate(%opts);

    if ($validate->{'is_valid'} && !$validate->{'is_expired'} 
        && (!$opts{'check_count'} || ($opts{'check_count'} && $validate->{'count'} == 1)))
    {
        return 1;
    }

    return undef;
}

# ��������� ��������� � ��������� ��� �������������
# ���������� ����. ��������� ���� ����������� � ������� �����.
#   time - ��� ���������� ������� ������ ���������� �� ��� ������ ���
#   allow_generate_code - ��������� ������������ ���, ���� �� �� ������
sub get_secret_code {
    my %opts = @_;

    # ������� ������� memcached �������� ������������ 
    # �������� �����������������
    my $memd = blessed($opts{'memd'}) ? $opts{'memd'} : $keeper->MEMD();
    return unless blessed($memd);

    my $time        = abs(int($opts{'time'}));
    my $now_time    = time();

    # ����� � ����������� �� ������ ����
    unless ($time) {
        $time = $now_time;
        $time = $time - ($time % 3600);
    }

    my $cache_key = 'secret_code|'.$time;

    my $secret_code = $memd->get($cache_key);
    return ($time, $secret_code) if $secret_code;

    if ($opts{'allow_generate_code'}) {
        $secret_code = &get_random_string(10);
        $memd->set($cache_key, $secret_code);
    }

    return ($time, $secret_code);
}


# ��������� ��������� ������
#   length - ����� ������
sub get_random_string {
    my $length = shift;
    $length = 10 unless $length && $length =~ /^\d+$/;

    my $random_chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
    my $random_chars_length = length($random_chars);

    my $string = '';
    for (1..$length) {
        $string .= substr($random_chars, int(rand($random_chars_length)), 1);
    }

    return $string;
}


1;
__END__

=head1 NAME

Utils::Spam::SecretForm - ������� ��������� � �������� ��������� ������ ��� web-����

=head1 SYNOPSIS

 ���������:
 <input type="hidden" name="secret" value="<% Utils::Spam::SecretForm::generate() %>">
  
 ��������:
 my $validate = Utils::Spam::SecretForm::validate( secret => $ARGS{'secret'}, check_count => 0|1 );
 if ($validate->{'is_valid'} && !$validate->{'is_expired'}) {
    allowed method
 }


=head1 DESCRIPTION

 � ������� javascript ���������� ������ �������� http-������� get � post �������.
 ����� �������� a.html ��������:
 <form method=post action=http://www.rambler.ru/post.html name="b">
 <input type="text" name="text">
 <input name="submit" type="submit" value="submit">
 </form>
 <script>
 document.b.submit.click();
 </script>
 
 ����� �������, �������, �������� �� �������� a.html, �������� ������ �����.
 ��� ������ � ������ ����� ���� ������� ������������ ������ ������.

 ������� ������: ��� � ��� ������������ ��������� ��������� ������, ������� �������� � ���� �� ����� secret_code|�����_���������.
 ���� ��� �� ��������, ��������� �������� ������� ��� ������ ���� ��������.
 ������������ ��� ��������� ����� �������� ����� ��� � hidden ����, ������� ������� �� ���� ������: 
   1) ����� ��������� ��������� ������
   2) ����� ������ ������ ������������
   3) ��������� ������
   4) md5_hex( ����� ������ ������ ������������ . ��������� ������ . ��������� ������)
 ��� ����������� ����������� hidden-��������� ���������� ��������� ��������� ������ �� ���� secret_code|�����_���������, ��������� md5_hex(������� . ���������� ������) � �������� ������� ������� ��������� ���� � �������� �������.
 ����� ������� �������� ������ �� ������������������� ��������. ��� ������ �� ���������� ������������� ���� ���������� ������������ �������� check_count, � ����������� �� �������� � ���������� ����������� �������� ���������� ������������� ���� (����� ������� ������������� count = 1).
 ��� ��������� ������ ���������� ����������� ���������� extra ��������� ��� ��������� hidden-����, �������� ��� ������������ ������������, ��� ����� ���� ����� ��� ������. �� ���������� �� �������� ���������� ���� �������� �� ����� ���������.