File Coverage

blib/lib/Authen/Quiz.pm
Criterion Covered Total %
statement 82 93 88.1
branch 15 30 50.0
condition 12 37 32.4
subroutine 19 20 95.0
pod 5 5 100.0
total 133 185 71.8


line stmt bran cond sub pod time code
1             package Authen::Quiz;
2             #
3             # Masatoshi Mizuno E<lt>lusheE<64>cpan.orgE<gt>
4             #
5             # $Id: Quiz.pm 361 2008-08-18 18:29:46Z lushe $
6             #
7 5     5   1274 use strict;
  5         10  
  5         182  
8 5     5   28 use warnings;
  5         7  
  5         171  
9 5     5   36 use File::Spec;
  5         9  
  5         108  
10 5     5   4289 use Digest::SHA1 qw/ sha1_hex /;
  5         4654  
  5         399  
11 5     5   38 use Carp qw/ croak /;
  5         10  
  5         256  
12 5     5   27 use base qw/ Class::Accessor::Fast /;
  5         9  
  5         4528  
13              
14             eval { require YAML::Syck; }; ## no critic.
15             if (my $error= $@) {
16             $error=~m{Can\'t\s+locate\s+YAML.+?Syck}i || die $error;
17             require YAML;
18             *load_quiz= sub { YAML::LoadFile( $_[0]->quiz_yaml ) };
19             } else {
20 7     7   29 *load_quiz= sub { YAML::Syck::LoadFile( $_[0]->quiz_yaml ) };
21             }
22              
23             our $VERSION = '0.05';
24              
25             __PACKAGE__->mk_accessors(qw/ data_folder expire session_id session_file /);
26              
27             our $QuizYaml = 'authen_quiz.yaml';
28             our $QuizSession= 'authen_quiz_session.txt';
29              
30 8     8 1 35 sub quiz_yaml { File::Spec->catfile( $_[0]->data_folder, $QuizYaml ) }
31              
32             sub new {
33 3     3 1 54 my $class = shift;
34 3 50 0     18 my $option= $_[1] ? {@_}: ($_[0] || croak __PACKAGE__. ' - I want option.');
35 3   50     22 $option->{expire} ||= 30; ## minute.
36 3 50       16 $option->{data_folder} || die __PACKAGE__. " - 'data_folder' is empty.";
37 3         17 $option->{data_folder}=~s{[\\\/\:]+$} [];
38 3         40 $option->{session_file}=
39             File::Spec->catfile($option->{data_folder}, $QuizSession);
40 3 50       79 -e $option->{session_file}
41             || die __PACKAGE__. " - Session file is not found. : $option->{session_file}";
42 3 50       66 -w $option->{session_file}
43             || die __PACKAGE__. " - I want write permission to session file.";
44 3         24 bless $option, $class;
45             }
46             sub question {
47 4     4 1 13 my($self)= @_;
48 4         18 my $quiz= $self->load_quiz;
49 4         950 my $keynow= do { my @list= keys %$quiz; $list[ int( rand(@list) ) ] };
  4         17  
  4         22  
50 4   33     17 my $data= $quiz->{$keynow}
51             || croak __PACKAGE__. " - Quiz data is empty. [$keynow]";
52 4 50       16 $data->[0] || croak __PACKAGE__. " - question data is empty. [$keynow]";
53 4 50       12 $data->[1] || croak __PACKAGE__. " - answer data is empty. [$keynow]";
54 4         27 my $limit= $self->_limit_time;
55 4   50     158 my $sha1now= $self->session_id( sha1_hex(
56             ($ENV{REMOTE_ADDR} || '127.0.0.1'). time. $$. rand(1000). $data->[0]
57             ) );
58             $self->_save(sub {
59 4     4   12 my($fh)= @_;
60 4         7 my $new_session;
61 4         70 for (<$fh>) {
62 1         12 my($T, $sha1, $key)= $self->_parse($_);
63 1 50 33     9 next if (! $T or $T< $limit);
64 1         132 $new_session.= $self->_session_line($T, $sha1, $key);
65             }
66 4         161 truncate $fh, 0;
67 4         23 seek $fh, 0, 0;
68 4   100     51 print $fh ($new_session || '')
69             . $self->_session_line(time, $sha1now, $keynow);
70 4         61 });
71 4         56 $data->[0];
72             }
73             sub check_answer {
74 2     2 1 9 my $self = shift;
75 2   33     6 my $sid = shift || croak __PACKAGE__. ' - I want session id.';
76 2   33     5 my $answer = shift || croak __PACKAGE__. ' - I want answer.';
77 2         4 my($quiz, $limit)= ($self->load_quiz, $self->_limit_time);
78 2         13 my $result;
79             $self->_save(sub {
80 2     2   6 my($fh)= @_;
81 2         2 my $new_session;
82 2         31 for (<$fh>) {
83 1         5 my($T, $sha1, $key)= $self->_parse($_);
84 1 50 33     11 next if (! $T or $T< $limit);
85 1 50       4 if ($sid eq $sha1) {
86 1 50       5 if (my $data= $quiz->{$key}) {
87 1 50 33     10 $result= 1 if ($data->[1] and $answer eq $data->[1]);
88             }
89             } else {
90 0         0 $new_session.= $self->_session_line($T, $sha1, ${key});
91             }
92             }
93 2         78 truncate $fh, 0;
94 2         12 seek $fh, 0, 0;
95 2   50     21 print $fh ($new_session || "");
96 2         12 });
97 2 100       28 $result || 0;
98             }
99             sub remove_session {
100 1     1 1 2 my $self = shift;
101 1 50       7 if (my $sid= shift) {
102 0         0 my $limit= $self->_limit_time;
103             $self->_save(sub {
104 0     0   0 my($fh)= @_;
105 0         0 my @data= <$fh>;
106 0         0 truncate $fh, 0;
107 0         0 seek $fh, 0, 0;
108 0         0 for (@data) {
109 0         0 my($T, $sha1, $key)= $self->_parse($_);
110 0 0 0     0 next if (! $T or $T< $limit or $sid eq $sha1);
      0        
111 0         0 print $fh $self->_session_line($T, $sha1, $key);
112             }
113 0         0 });
114             } else {
115 1     1   6 $self->_save(sub { truncate $_[0], 0 });
  1         41  
116             }
117 1         78 $self;
118             }
119 2 50   2   21 sub _parse { $_[1] ? $_[1]=~m{^(.+?)\t(.+?)\t([^\n]+)}: '' }
120 5     5   33 sub _session_line { "$_[1]\t$_[2]\t$_[3]\n" }
121 6     6   392 sub _limit_time { time- ($_[0]->expire* 60) }
122             sub _save {
123 7     7   14 my($self, $code)= @_;
124 7   50     10 open QUIZ, "+<@{[ $self->session_file ]}" ## no critic.
125             || die __PACKAGE__. " - File open error: @{[ $self->session_file ]}";
126 7         308 flock QUIZ, 2; # write lock.
127 7         17 $code->(*QUIZ);
128 7         197 close QUIZ;
129 7         14 $self;
130             }
131              
132             1;
133              
134             __END__
135              
136             =head1 NAME
137              
138             Authen::Quiz - The person's input is confirmed by setting the quiz.
139              
140             =head1 SYNOPSIS
141              
142             use Authen::Quiz;
143            
144             my $q= Authen::Quiz->new(
145             data_folder => '/path/to/authen_quiz', ## Passing that arranges data file.
146             expire => 30, ## Expiration date of setting questions(amount).
147             );
148            
149             ## Setting of quiz.
150             my $question= $q->question;
151            
152             ## When 'question' method is called, 'session_id' is set.
153             ## This value is buried under the form, and it passes it to 'check_answer' method later.
154             my $session_id= $q->session_id;
155            
156             #
157             ## Check on input answer.
158             my $session_id = $cgi->param('quiz_session') || return valid_error( ..... );
159             my $answer = $cgi->param('quiz_answer') || return valid_error( ..... );
160             if ($q->check_answer($session_id, $answer)) {
161             # ... is success.
162             } else {
163             return valid_error( ..... );
164             }
165              
166             =head1 DESCRIPTION
167              
168             This module sets the quiz to the input of the form, and confirms whether it is artificially done.
169              
170             Recently, to take the place of it because there seemed to be a thing that the capture attestation
171             is broken by improving the image analysis technology, it produced it.
172              
173             Moreover, I think that it can limit the user who can use the input form if the difficulty of the
174             quiz is adjusted.
175              
176             =head2 Method of checking artificial input.
177              
178             =head3 1. Setting of problem.
179              
180             The problem of receiving it by the question method is displayed on the screen.
181              
182             ID received by the session_id method is set in the hidden field of the input form.
183              
184             =head3 2. Confirmation of input answer.
185              
186             The answer input to the check_answer method as session_id of 1. is passed, and whether
187             it agrees is confirmed.
188              
189             =head2 Preparation for quiz data of YAML form.
190              
191             First of all, it is necessary to make use the quiz data of the following YAML formats.
192              
193             ---
194             F01:
195             - What color is the color of the apple ?
196             - red
197             F02:
198             - What color is the color of the lemon ?
199             - yellow
200             F03:
201             - The color of the orange and the cherry ties by '+' and is answered.
202             - orange+red
203              
204             'F01' etc. It is an identification name of the quiz data. Any name is not and is not
205             cared about by the rule if it is a unique name.
206              
207             And, the first element in ARRAY becomes the value under the control of the identification name
208             and "Problem" and the second element are made to become to "Answer".
209              
210             The file of the name 'authen_quiz.yaml' is made under the control of 'data_folder'
211             when completing it.
212              
213             =head2 Preparation for session data.
214              
215             Permission that can make the empty file of the name 'authen_quiz_session.txt', and write it
216             from CGI script is set.
217              
218             The preparation is completed by this.
219              
220             Please produce the part of the WEB input form and the input confirmation according
221             to this now.
222              
223              
224             =head1 METHODS
225              
226             =head2 new ([OPTION_HASH])
227              
228             Constructor.
229              
230             HASH including the following items is passed as an option.
231              
232             =over 4
233              
234             =item * data_folder
235              
236             Passing of place where data file was arranged.
237              
238             There is no default. Please specify it.
239              
240             =item * expire
241              
242             The expiration date of setting questions is set in each amount.
243              
244             Default is 30 minutes.
245              
246             =back
247              
248             my $q= Authen::Quiz->new(
249             data_folder => '/path/to/temp',
250             expire => 60,
251             );
252              
253             =head2 quiz_yaml
254              
255             Passing the quiz data is returned.
256              
257             * It is a value that returns in which $QuizYaml ties to 'data_folder'.
258              
259             To change the file name, the value is set in $QuizYaml.
260              
261             $Authen::Quiz::QuizYaml = 'orign_quiz.yaml';
262              
263             =head2 session_file
264              
265             Passing the session data is returned.
266              
267             * It is a value that returns in which $QuizSession ties to 'data_folder'.
268              
269             To change the file name, the value is set in $QuizSession.
270              
271             $Authen::Quiz::QuizSession = 'orign_quiz_session.txt';
272              
273             =head2 load_quiz
274              
275             The quiz data of the YAML form is loaded.
276              
277             my $quiz_data= $q->load_quiz;
278              
279             =head2 question
280              
281             The question displayed in the input form is set.
282              
283             This method sets a unique HEX value in session_id at the same time.
284              
285             my $question= $q->question;
286              
287             =head2 session_id
288              
289             It is made to succeed by setting the value received by this method in the hidden field of
290             the input form.
291              
292             When the check_answer method is called, this value is needed.
293              
294             my $question = $q->question;
295             my $session_id = $q->session_id;
296              
297             =head2 check_answer ([SESSION_ID], [ANSWER_STRING])
298              
299             It checks whether the answer input to the form is correct.
300              
301             The value received by the session_id method is passed to SESSION_ID.
302              
303             The input data is passed to ANSWER_STRING as it is.
304              
305             * It is a caution needed because it doesn't do Validation.
306              
307             my $session_id = validate($cgi->param('quiz_session')) || return valid_error( ..... );
308             my $answer = validate($cgi->param('quiz_answer')) || return valid_error( ..... );
309             if ($q->check_answer($session_id, $answer)) {
310             # success.
311             } else {
312             return valid_error( ..... );
313             }
314              
315             =head2 remove_session ([SESSION_ID])
316              
317             The data of the quiz session is deleted.
318              
319             When SESSION_ID is omitted, all data is deleted.
320              
321             $q->session_remove( $session_id );
322              
323              
324             =head1 OTHERS
325              
326             There might be a problem in the response because it reads the quiz data every time.
327             If the Wrapper module is made and cash is used, this can be solved.
328              
329             package MyAPP::AuthQuizWrapper;
330             use strict;
331             use warnings;
332             use Cache::Memcached;
333             use base qw/ Authen::Quiz /;
334            
335             sub load_quiz {
336             my $cache= Cache::Memcached->new;
337             $cache->get('authen_quiz_data') || do {
338             my $data= $_[0]->SUPER::load_quiz;
339             $cache->set('authen_quiz_data'=> $data, 600);
340             data;
341             };
342             }
343            
344             1;
345              
346             Or, please use L<Authen::Quiz::Plugin::Memcached>.
347              
348             =head1 SEE ALSO
349              
350             L<Carp>,
351             L<Class::Accessor::First>,
352             L<Digest::SHA1>,
353             L<File::Spec>,
354             L<YAML::Syck>,
355             L<YAML>,
356              
357             L<http://egg.bomcity.com/wiki?Authen%3a%3aQuiz>,
358              
359             =head1 AUTHOR
360              
361             Masatoshi Mizuno E<lt>lusheE<64>cpan.orgE<gt>
362              
363             =head1 COPYRIGHT AND LICENSE
364              
365             Copyright (C) 2008 by Bee Flag, Corp. E<lt>http://egg.bomcity.com/E<gt>.
366              
367             This library is free software; you can redistribute it and/or modify
368             it under the same terms as Perl itself, either Perl version 5.8.8 or,
369             at your option, any later version of Perl 5 you may have available.
370              
371             =cut
372