LJSUP-17669: Login.bml form refactoring
[livejournal.git] / cgi-bin / LJ / Captcha.pm
blob2805c847d5a0e8ba06bf8d31d99467b588b35681
1 #!/usr/bin/perl
3 package LJ::Captcha;
4 use strict;
5 use LJ::Blob qw{};
7 use lib "$ENV{LJHOME}/cgi-bin";
8 use LJ;
9 use LJ::Auth::Challenge;
11 ### get_visual_id() -> ( $capid, $anum )
12 sub get_visual_id { get_id('image') }
13 sub get_audio_id { get_id('audio') }
15 ### get_id( $type ) -> ( $capid, $anum )
16 sub get_id
18 my ( $type ) = @_;
19 my (
20 $dbh, # Database handle (writer)
21 $sql, # SQL statement
22 $row, # Row arrayref
23 $capid, # Captcha id
24 $anum, # Unseries-ifier number
25 $issuedate, # unixtime of issue
28 # Fetch database handle and lock the captcha table
29 $dbh = LJ::get_db_writer()
30 or return LJ::error( "Couldn't fetch a db writer." );
31 $dbh->selectrow_array("SELECT GET_LOCK('get_captcha', 10)")
32 or return LJ::error( "Failed lock on getting a captcha." );
34 # Fetch the first unassigned row
35 $sql = q{
36 SELECT capid, anum
37 FROM captchas
38 WHERE
39 issuetime = 0
40 AND type = ?
41 LIMIT 1
43 $row = $dbh->selectrow_arrayref( $sql, undef, $type )
44 or $dbh->do("DO RELEASE_LOCK('get_captcha')") && die "No $type captchas available";
45 die "selectrow_arrayref: $sql: ", $dbh->errstr if $dbh->err;
46 ( $capid, $anum ) = @$row;
48 # Mark the captcha as issued
49 $issuedate = time();
50 $sql = qq{
51 UPDATE captchas
52 SET issuetime = $issuedate
53 WHERE capid = $capid
55 $dbh->do( $sql ) or die "do: $sql: ", $dbh->errstr;
56 $dbh->do("DO RELEASE_LOCK('get_captcha')");
58 return ( $capid, $anum );
62 ### get_visual_data( $capid, $anum, $want_paths )
63 # if want_paths is true, this function may return an arrayref containing
64 # one or more paths (disk or HTTP) to the resource
65 sub get_visual_data
67 my ( $capid, $anum, $want_paths ) = @_;
68 $capid = int($capid);
70 my (
71 $dbr, # Database handle (reader)
72 $sql, # SQL statement
73 $valid, # Are the capid/anum valid?
74 $data, # The PNG data
75 $u, # System user
76 $location, # Location of the file (mogile/blob)
79 $dbr = LJ::get_db_reader();
80 $sql = q{
81 SELECT capid, location
82 FROM captchas
83 WHERE
84 capid = ?
85 AND anum = ?
88 ( $valid, $location ) = $dbr->selectrow_array( $sql, undef, $capid, $anum );
89 return undef unless $valid;
91 if ($location eq 'mogile') {
92 die "MogileFS object not loaded.\n" unless LJ::mogclient();
93 if ($want_paths) {
94 # return path(s) to the content if they want
95 my @paths = LJ::mogclient()->get_paths("captcha:$capid");
96 return \@paths;
97 } else {
98 $data = ${LJ::mogclient()->get_file_data("captcha:$capid")};
100 } else {
101 $u = LJ::load_user( "system" )
102 or die "Couldn't load the system user.";
104 $data = LJ::Blob::get( $u, 'captcha_image', 'png', $capid )
105 or die "Failed to fetch captcha_image $capid from media server";
107 return $data;
111 ### get_audio_data( $capid, $anum, $want_paths )
112 # if want_paths is true, this function may return an arrayref containing
113 # one or more paths (disk or HTTP) to the resource
114 sub get_audio_data
116 my ( $capid, $anum, $want_paths ) = @_;
117 $capid = int($capid);
119 my (
120 $dbr, # Database handle (reader)
121 $sql, # SQL statement
122 $valid, # Are the capid/anum valid?
123 $data, # The PNG data
124 $u, # System user
125 $location, # Location of the file (mogile/blob)
128 $dbr = LJ::get_db_reader();
129 $sql = q{
130 SELECT capid, location
131 FROM captchas
132 WHERE
133 capid = ?
134 AND anum = ?
137 ( $valid, $location ) = $dbr->selectrow_array( $sql, undef, $capid, $anum );
138 return undef unless $valid;
140 if ($location eq 'mogile') {
141 die "MogileFS object not loaded.\n" unless LJ::mogclient();
142 if ($want_paths) {
143 # return path(s) to the content if they want
144 my @paths = LJ::mogclient()->get_paths("captcha:$capid");
145 return \@paths;
146 } else {
147 $data = ${LJ::mogclient()->get_file_data("captcha:$capid")};
149 } else {
150 $u = LJ::load_user( "system" )
151 or die "Couldn't load the system user.";
153 $data = LJ::Blob::get( $u, 'captcha_audio', 'wav', $capid )
154 or die "Failed to fetch captcha_audio $capid from media server";
156 return $data;
159 ### check_code( $capid, $anum, $code, $u ) -> <true value if code is correct>
160 sub check_code {
161 my ( $capid, $anum, $code, $u ) = @_;
163 my (
164 $dbr, # Database handle (reader)
165 $sql, # SQL query
166 $answer, # Challenge answer
167 $userid, # userid of previous answerer (or 0 if none)
170 $sql = q{
171 SELECT answer, userid
172 FROM captchas
173 WHERE
174 capid = ?
175 AND anum = ?
178 # Fetch the challenge's answer based on id and anum.
179 $dbr = LJ::get_db_writer();
180 ( $answer, $userid ) = $dbr->selectrow_array( $sql, undef, $capid, $anum );
182 # if it's already been answered, it must have been answered by the $u
183 # given to this function (double-click protection)
184 return 0 if $userid && ( ! $u || $u->{userid} != $userid );
186 # otherwise, just check answer.
187 return lc $answer eq lc $code;
190 # Verify captcha answer if using a captcha session.
191 # (captcha challenge, code, $u)
192 # Returns capid and anum if answer correct. (for expire)
193 sub session_check_code {
194 my ($sess, $code, $u) = @_;
195 return 0 unless $sess && $code;
196 $sess = LJ::get_challenge_attributes($sess);
198 $u = LJ::load_user('system') unless $u;
200 my $dbcm = LJ::get_cluster_master($u);
201 my $dbr = LJ::get_db_reader();
203 my ($lcapid, $try) = # clustered
204 $dbcm->selectrow_array('SELECT lastcapid, trynum ' .
205 'FROM captcha_session ' .
206 'WHERE sess=?', undef, $sess);
207 my ($capid, $anum) = # global
208 $dbr->selectrow_array('SELECT capid,anum ' .
209 'FROM captchas '.
210 'WHERE capid=?', undef, $lcapid);
211 if (! LJ::Captcha::check_code($capid, $anum, $code, $u)) {
212 # update try and lastcapid
213 $u->do('UPDATE captcha_session SET lastcapid=NULL, ' .
214 'trynum=trynum+1 WHERE sess=?', undef, $sess);
215 return 0;
217 return ($capid, $anum);
220 ### expire( $capid ) -> <true value if code was expired successfully>
221 sub expire {
222 my ( $capid, $anum, $userid ) = @_;
224 my (
225 $dbh, # Database handle (writer)
226 $sql, # SQL update query
229 $sql = q{
230 UPDATE captchas
231 SET userid = ?
232 WHERE capid = ? AND anum = ? AND userid = 0
235 # Fetch the challenge's answer based on id and anum.
236 $dbh = LJ::get_db_writer();
237 $dbh->do( $sql, undef, $userid, $capid, $anum ) or return undef;
239 return 1;
242 # Update/create captcha sessions, return new capid/anum pairs on success.
243 # challenge, type, optional journalu->{clusterid} for clustering.
244 # Type is either 'image' or 'audio'
245 sub session {
246 my ($chal, $type, $cid) = @_;
247 return unless $chal && $type;
249 return unless LJ::Auth::Challenge->check($chal);
251 my $sess = LJ::get_challenge_attributes($chal);
252 my ($capid, $anum) = ($type eq 'image') ?
253 LJ::Captcha::get_visual_id() :
254 LJ::Captcha::get_audio_id();
257 $cid = LJ::load_user('system')->{clusterid} unless $cid;
258 my $dbcm = LJ::get_cluster_master($cid);
260 # Retain try count
261 my $try = $dbcm->selectrow_array('SELECT trynum FROM captcha_session ' .
262 'WHERE sess=?', undef, $sess);
263 $try ||= 0;
264 # Add/update session
265 $dbcm->do('REPLACE INTO captcha_session SET sess=?, sesstime=?, '.
266 'lastcapid=?, trynum=?', undef, $sess, time(), $capid, $try);
267 return ($capid, $anum);