Pay with Credit Card body<= {r}; my $uniq = $r->notes('uniq'); my $dbh = LJ::get_db_writer(); my $cart = $POST{'cart'}; my $remote = LJ::get_remote(); my $remote_ip = LJ::get_remote_ip(); return BML::redirect("$LJ::SITEROOT/pay") unless LJ::did_post(); my $no_charge = "As a result of this error, your credit card has not been charged."; my $error = sub { return LJ::bad_input($_[0], $no_charge); }; # is merchant gateway down? return "" . "" if $LJ::AUTHNET_DOWN; unless ($POST{'bill_country'} =~ /^\w+$/) { return $error->("Please Go back and enter your country."); } if ($POST{'bill_country'} eq "US") { return $error->("Zip code required for credit card verification. Please go back and enter your zip code.") unless $POST{'bill_zip'} =~ /\d+/; } my $lockname = "authnet-$cart"; my $gotlock = $dbh->selectrow_array("SELECT GET_LOCK(?,5)", undef, $lockname); unless ($gotlock) { return $error->("Error getting lock."); } my $unlock = sub { $dbh->do("SELECT RELEASE_LOCK(?)", undef, $lockname); }; # make $error unlock now $error = sub { $unlock->(); return LJ::bad_input($_[0], $no_charge); }; my $cartobj = LJ::Pay::load_cart($cart) or return $error->("Order not found."); # do time-based rate-limiting if (@LJ::MEMCACHE_SERVERS) { # start by checking rate keys for this cart my @memkeys = ([$cartobj->{payid}, "cctry_payid:$cartobj->{payid}"]); push @memkeys, [$cartobj->{userid}, "cctry_uid:$cartobj->{userid}"] if $cartobj->{userid}; push @memkeys, [$uniq, "cctry_uniq:$uniq"] if $uniq; # now check rate keys for all of the items, but don't note duplicate # email addresses/uids my %emails = (); my %userids = (); foreach my $it (@{$cartobj->{items}}) { if (my $uid = $it->{rcptid}) { next if $userids{$uid} || $uid == $cartobj->{userid}; $userids{$uid}++; push @memkeys, [$uid, "cctry_uid:$uid"]; } elsif (my $email = $it->{rcptemail}) { next if $emails{$email}; $emails{$email}++; push @memkeys, [$email, "cctry_email:$email"]; } } my $now = time(); my $cctry = LJ::MemCache::get_multi(@memkeys) || {}; my $exceeded = 0; # is the rate exceeded based on one of the time queues? my $sendmail = 0; # was the 10 try threshold passed? eg: send an email foreach my $memkey (@memkeys) { my $val = $cctry->{$memkey->[1]} || []; my @try = grep { $_ >= $now - 3600*6 } @$val; my $tries = scalar @try; my $lasttry = $try[-1]; my $toofast = LJ::run_hook("ccpay_rate_check", $tries, $lasttry); $exceeded = 1 if $toofast; $sendmail = 1 if $tries == 10; # passed into "probably bot" territory # save this try push @try, $now; @try = @try[-20..-1] if @try > 20; LJ::MemCache::set($memkey, \@try); } # send an email if it's a moderately severe violation # we send an email when they go over the 10 try mark, # then not again since if they got this far they'll try # multiple times and we don't want a flood of emails if ($sendmail) { my $passtxt; $passtxt = "NOTE: Although this user has made 10+ tries to process their credit card, " . "the most recent attempt (which triggered this email) has passed rate-limit " . "checks. The behavior is still fishy and may require further investigation.\n\n" unless $exceeded; LJ::send_mail({ 'to' => $LJ::ACCOUNTS_EMAIL, 'from' => $LJ::ACCOUNTS_EMAIL, 'wrap' => 1, 'subject' => "CC rate limiting: payid: $cartobj->{payid}", 'body' => "This warning has been sent because there have been at least 10 " . "attempts to pay for order \#$cartobj->{payid} via credit card. " . "No further emails will be sent about this incident since they are likely " . "to continue trying, but here is relevant sysban information: \n\n" . " Payid: $cartobj->{payid}\n" . " Remote User: " . ($remote ? $remote->{user} : 'unknown') . "\n" . " Remote IP: " . ($remote_ip || 'unknown') . "\n" . " Uniq Cookie: " . ($uniq || 'unknown') . "\n\n" . "Email addresses and userids have also been included in the rate checking. " . "See $LJ::SITEROOT/admin/accounts/paiddetails.bml?payid=$cartobj->{payid} " . "for more information.\n\n" . $passtxt, }); } # see if they're voilating rate limits if ($exceeded) { return $error->("You are attempting to process credit card transactions too frequently. " . "Your credit card has not been charged. Please wait a while then try again " . "later. If the problem persists, please contact $LJ::ACCOUNTS_EMAIL."); } } # verify cart (check for items that have been invalidated since adding) unless (LJ::Pay::is_valid_cart($cartobj)) { return $error->("The given cart contains items that are no longer valid. This could be caused by conflicts with other " . "carts created during the same time. Please try again with a new cart."); } # now we have a cart object if ($cartobj->{'userid'}) { return $error->("You must be logged in as this cart's owner to proceed with checkout.") unless $remote; return $error->("You do not own this cart.") unless $cartobj->{'userid'} == $remote->{'userid'}; if (LJ::sysban_check('pay_email', $remote->{'email'})) { LJ::sysban_note($remote->{'userid'}, "Payment blocked based on email", { 'email' => $remote->{'email'}, 'user' => $remote->{'user'}, 'cart' => $cart }); return $error->("Sorry, we were unable to process your request."); } if (LJ::sysban_check('pay_user', $remote->{'user'})) { LJ::sysban_note($remote->{'userid'}, "Payment blocked based on user", { 'user' => $remote->{'user'}, 'cart' => $cart }); return $error->("Sorry, we were unable to process your request."); } } if ($uniq && LJ::sysban_check('pay_uniq', $uniq)) { LJ::sysban_note($remote ? $remote->{'userid'} : 0, "Payment blocked based on uniq", { 'user' => $remote ? $remote->{'user'} : "(anon)", 'uniq' => $uniq, 'cart' => $cart }); return $error->("Sorry, we were unable to process your request."); } # if there is no userid, then we know it's clean because we checked # each individual item for email blocks when they were added to the cart my $has_coppa = LJ::Pay::cart_contains_coppa($cartobj); my $needs_shipping = LJ::Pay::cart_needs_shipping($cartobj); # coppa requires remote if ($has_coppa && ! $remote) { return $error->("You must be logged in as the account being verified (the child's account) in order to process an order containing an Age Verification item."); } my $amt_total = $cartobj->{amount}; my $coppa_only = 0; if ($amt_total <= 0 && $has_coppa) { $amt_total = '0.01'; $coppa_only = 1; } return $error->("Order amount is zero.") unless $amt_total > 0; my $u; if ($cartobj->{'userid'}) { $u = LJ::load_userid($cartobj->{'userid'}); LJ::load_user_props($u, 'allow_pay', 'fraud_watch'); } my $country = LJ::LJcom::country_of_ip($remote_ip); my $now = LJ::mysql_time(time()); LJ::Pay::payvar_add($cartobj->{'payid'}, "geoip-country", "$now: country=$country ($remote_ip)"); # attempt to gather and store user's country/state if ($POST{bill_country}) { my $err; my ($ctry, $st) = LJ::Pay::check_country_state($POST{bill_country}, $POST{bill_state}, \$err); return $error->($err) if $err; # now save country and state in database LJ::Pay::payid_set_state($cartobj->{payid}, $ctry, $st); } my $from = sub { return lc($country) eq $_[0] || lc($POST{'bill_country'}) eq $_[0] }; # stop Russian/Ukrainian credit card theft if ($from->('ru') || $from->('ua')) { # see if we should force the user to be allowed to pay unless ($u && $u->{'allow_pay'} eq 'Y') { # if cart needs shipping, don't accept any card or address from Russia/Ukraine return $error->("We do not currently accept credit card payments from Russia or the Ukraine on shipped goods.") if $needs_shipping; # if cart doesn't need shipping, bill country and country must match if either is Russia/Ukraine return $error->("The country of your Internet address does not match the country of your credit card. From here you can:") unless lc($country) eq lc($POST{'bill_country'}); } # otherwise we've decided to allow this user, log LJ::Pay::payvar_add($cartobj->{'payid'}, "allow_pay", "$now: cc=$POST{'bill_country'}, actual=$country, user=$u->{'user'} (allowed)"); } # throw an error if they didn't provide a parent's email my $paremail; if ($has_coppa) { $paremail = $POST{paremail}; my @email_errors; push @email_errors, "You must enter a parent email address to proceed with COPPA Age Verification." unless $paremail; push @email_errors, "The parental email address must not match your account's email address." if lc $remote->{email} eq lc $paremail; # ignore case changes LJ::check_email($paremail, \@email_errors); # throw message if necessary return $error->(@email_errors) if @email_errors; } my $setup_coppa_email_validation = sub { # register an auth action my $aa = LJ::register_authaction($remote->{userid}, "parentemail", $paremail) or return $error->("Unable to register authaction."); # now send the verification email LJ::send_mail({ to => $paremail, from => $LJ::SUPPORT_EMAIL, subject => "$LJ::SITENAME Parent Email Verification", wrap => 1, body => qq{ Welcome, you have attempted to create a $LJ::SITENAME account for your child. By activating this account you certify that you have read our Children's Online Privacy Protection Act disclosure, Terms of Service and Privacy Policy and that you agree to them. COPPA Disclosure $LJ::SITEROOT/legal/coppa.bml Terms of Service $LJ::SITEROOT/legal/tos.bml Privacy Policy $LJ::SITEROOT/legal/privacy.bml To complete your child's journal creation and verify your email address, you must click on the link below. $LJ::SITEROOT/agecheck/consent.bml?c=$aa->{aaid}.$aa->{authcode} (If you are unable to click on the link, copy it into your browser.) Your child may access his or her journal at the following two URLs: $LJ::SITEROOT/users/$remote->{user}/ $LJ::SITEROOT/~$remote->{user}/ Below is the $LJ::SITENAME username that was registered: Username: $remote->{user} If your child needs to retrieve his or her password, they can do so at any time by visiting the following URL: $LJ::SITEROOT/lostinfo.bml Enjoy! $LJ::SITENAME Team $LJ::SITEROOT/ }, }); }; my $success = sub { my $ret; if ($coppa_only) { $ret .= ""; $ret .= "COPPA Age Verification. Parents, please check your email: you should shortly receive a verification message. As soon as you get the email and click the link inside, your child's account will be fully active. p?>"; $setup_coppa_email_validation->(); } else { $ret .= ""; $ret .= ""; if ($has_coppa) { $ret .= "COPPA Age Verification. Parents, please check your email: you should shortly receive a verification email. As soon as you get the email and click the link inside, your child's account will be fully active. p?>"; $setup_coppa_email_validation->(); } } if ($needs_shipping) { $ret .= "Notice: Your email receipt from Authorize.net (our credit card processor) won't contain shipping information. Don't worry... that information is stored in our database. p?>"; } $ret .= " 1 }); $ret .= " p?>"; $unlock->(); return $ret; }; if ($cartobj->{'mailed'} ne "C") { # not in cart state anymore. # double-clicked? tell them it's all good. return $success->(); } my $amt_charge = sprintf("%0.02f", $amt_total); if ($amt_charge ne $POST{'amt_charge'}) { return $error->("Unexpected error: order amount changed from time you entered card info until now."); } my $cardnum = $POST{'cardnum'}; $cardnum =~ s![^\d]!!g; # remove non-numbers my $expdate = sprintf("%02d%02d", $POST{'expdate_mm'}, $POST{'expdate_yy'} % 100); my $cardcode = $POST{'cardcode'}; $cardcode =~ s!\s+!!g; # remove non-numbers if ($LJ::USE_CARD_CODE) { return $error->("You must provide your full card verification number for this transaction.") unless $cardcode =~ /^\d{3,}$/; } # blocked credit card? $cardnum =~ /^(\d\d\d\d).*(\d\d\d\d)$/; if (LJ::sysban_check('pay_cc', "$1-$2")) { my $vars = { 'cart' => $cart }; # no remote, there's not much useful information # so we'll see who they were buying for unless ($remote) { # get usernames of people they're trying to buy for my %rcpt_users = (); my $ct = 0; LJ::load_userids_multiple([ map { $_->{'rcptid'} => \$rcpt_users{"rcpt_user_" . ++$ct} } @{$cartobj->{'items'}} ]); # fill in vars to log $ct = 0; map { $ct++, $vars->{"rcpt_email_$ct"} = $_->{'rcptemail'} if $_->{'rcptemail'} } @{$cartobj->{'items'}}; $ct = 0; map { $ct++, $vars->{"rcpt_user_$ct"} = $rcpt_users{$_}->{'user'} if $rcpt_users{$_}->{'user'} } keys %rcpt_users; } else { $vars->{'email'} => $remote->{'email'}; $vars->{'user'} => $remote->{'user'}; } LJ::sysban_note($remote ? $remote->{'userid'} : 0, "Payment blocked based on credit card", $vars); return $error->("We are unable to process the card given."); } # send fraud watch emails if necessary LJ::Pay::send_fraud_email($cartobj, $u); # AUTHORIZE: see if the AVS information is accurate the the account is in good standing my $ua = new LWP::UserAgent; $ua->agent("LJ-AuthNet/1.0"); my $vars = { 'x_Version' => '3.1', 'x_Delim_Data' => 'True', 'x_Login' => $LJ::AUTHNET_USER, 'x_Password' => $LJ::AUTHNET_PASS, 'x_Card_Num' => $cardnum, 'x_Exp_Date' => $expdate, 'x_Amount' => $amt_charge, 'x_Email' => $POST{'email'}, 'x_Email_Customer' => ($coppa_only ? 'FALSE' : 'TRUE'), # don't email receipts for coppa verifications 'x_Customer_IP' => $remote_ip, 'x_Cust_ID' => $cartobj->{'userid'}, 'x_Type' => 'AUTH_ONLY', 'x_Invoice_Num' => $cart, # max 20, big enough 'x_Address' => $POST{'bill_addr'}, 'x_City' => $POST{'bill_city'}, 'x_State' => $POST{'bill_state'}, 'x_Zip' => $POST{'bill_zip'}, 'x_Country' => $POST{'bill_country'}, 'x_Merchant_Email' => $LJ::AUTHNET_MERCHANT, }; $vars->{'x_Card_Code'} = $cardcode if $LJ::USE_CARD_CODE; $vars->{'x_Test_Request'} = 'TRUE' if $LJ::AUTHNET_TEST; my $req = new HTTP::Request POST => 'https://secure.authorize.net/gateway/transact.dll'; $req->content_type('application/x-www-form-urlencoded'); $req->content(join("&", map { LJ::eurl($_) . "=" . LJ::eurl($vars->{$_}) } keys %$vars)); # Pass request to the user agent and get a response back my $res = $ua->request($req); my ($ct, $auth_pass, $err); my @fields; if ($res->is_success) { $ct = $res->content; # did authorization pass? @fields = split(/,/, $ct); $auth_pass = $fields[0] == 1; $err = $fields[3] unless $auth_pass; } else { $err = "Could not contact payment gateway."; } my $txn_id = $fields[6]; $dbh->do("INSERT INTO authnetlog (payid, cmd, datesent, ip, amt, result, response) ". "VALUES (?,'authonly',NOW(),?,?,?,?)", undef, $cartobj->{'payid'}, $remote_ip, $amt_charge, $auth_pass ? "pass" : "fail", $ct); my %payvars = ( 'an-ip' => $remote_ip, 'an-email' => $POST{'email'}, 'an-cardname' => $POST{'cardname'}, ); $payvars{'an-avs'} = join('|', map { $POST{"bill_$_"} } qw(addr city state zip country)); my $cap_pass; my $cap_txn_id; # CAPTURE: now it's time to get the money, if there were no AVS errors if ($auth_pass && ! $coppa_only) { my $vars = { 'x_Version' => '3.1', 'x_Delim_Data' => 'True', 'x_Login' => $LJ::AUTHNET_USER, 'x_Password' => $LJ::AUTHNET_PASS, 'x_Trans_ID' => $txn_id, 'x_Type' => 'PRIOR_AUTH_CAPTURE', }; $vars->{'x_Test_Request'} = 'TRUE' if $LJ::AUTHNET_TEST; my $req = new HTTP::Request POST => 'https://secure.authorize.net/gateway/transact.dll'; $req->content_type('application/x-www-form-urlencoded'); $req->content(join("&", map { LJ::eurl($_) . "=" . LJ::eurl($vars->{$_}) } keys %$vars)); my $res = $ua->request($req); my $ct; my @fields; if ($res->is_success) { $ct = $res->content; # did capture pass? @fields = split(/,/, $ct); $cap_pass = $fields[0] == 1; $err = $fields[3] unless $cap_pass; } else { $err = "Could not contact payment gateway."; } $cap_txn_id = $fields[6]; $dbh->do("INSERT INTO authnetlog (payid, cmd, datesent, ip, amt, result, response) ". "VALUES (?,'priorcap',NOW(),?,?,?,?)", undef, $cartobj->{'payid'}, $remote_ip, $amt_charge, $cap_pass ? "pass" : "fail", $ct); # keep track of trans ID & card fingerprint to enable refunds if necessary if ($cap_pass) { $cardnum =~ /^(\d\d\d\d).*(\d\d\d\d)$/; $payvars{'an-refund'} = join(",", $cap_txn_id, $expdate, $1, $2); $dbh->do("UPDATE payments SET used='N', mailed='N', ". "method='cc', daterecv=NOW() WHERE payid=?", undef, $cartobj->{'payid'}); } # VOID: if capture failed, uncapture their fund # 0: 2 means declined # 1: 2 means declined # 3: 27 means AVS mismat if ($fields[0] == 2 && $fields[1] == 2 && $fields[2] == 27 && $txn_id) { my $vars = { 'x_Version' => '3.1', 'x_Delim_Data' => 'True', 'x_Login' => $LJ::AUTHNET_USER, 'x_Password' => $LJ::AUTHNET_PASS, 'x_Trans_ID' => $txn_id, 'x_Type' => 'VOID', }; $vars->{'x_Test_Request'} = 'TRUE' if $LJ::AUTHNET_TEST; my $req = new HTTP::Request POST => 'https://secure.authorize.net/gateway/transact.dll'; $req->content_type('application/x-www-form-urlencoded'); $req->content(join("&", map { LJ::eurl($_) . "=" . LJ::eurl($vars->{$_}) } keys %$vars)); my $res = $ua->request($req); my $ct; if ($res->is_success) { $ct = $res->content; } my @fields = split(/,/, $ct); my $pass = $fields[0] == 1; $dbh->do("INSERT INTO authnetlog (payid, cmd, datesent, ip, amt, result, response) ". "VALUES (?,'void',NOW(),?,?,?,?)", undef, $cartobj->{'payid'}, $remote_ip, 0.00, $pass ? "pass" : "fail", $ct); } } # save payvars in db now foreach (keys %payvars) { next unless $payvars{$_}; LJ::Pay::payvar_add($cartobj->{'payid'}, $_, $payvars{$_}); } # if a failure, note in ccfail table and possibly notify admins unless ($auth_pass && ($coppa_only || $cap_pass)) { # note that we've failed once again my %done; my $emailtxt; foreach my $it ([$cartobj->{'userid'}, $cartobj->{'rcptemail'}], map { [ $_->{'rcptid'}, $_->{'rcptemail'} ] } @{$cartobj->{'items'}}) { my $userid = $it->[0]; my $email = $it->[1]; my $user = $email; if ($userid) { my $u = LJ::load_userid($userid); $email = $u->{'email'}; $user = $u->{'user'}; } next if $done{$email}; $done{$email}++; # get failures within the last 30 days my $failct = $dbh->selectrow_array("SELECT COUNT(*) FROM ccfail " . "WHERE email=? AND time>UNIX_TIMESTAMP()-3600*24*30", undef, $email); # see if we've passed the threshold $failct++; if ($LJ::CCFAIL_NOTIFY && $failct % $LJ::CCFAIL_NOTIFY == 0) { my $now = LJ::mysql_time(); my $lastmailct = $failct - $LJ::CCFAIL_NOTIFY; # pretty good guess $emailtxt .= " User: " . ($user || $email) . "\n" . " Failures: $failct\n" . " Last Mail: $lastmailct failures\n" . " Payid: $cartobj->{'payid'}\n" . " Time: $now\n\n"; } # now update table in db $dbh->do("REPLACE INTO ccfail (email, time, userid) " . "VALUES (?, UNIX_TIMESTAMP(), ?)", undef, $email, $userid); } # send out the combined email now LJ::send_mail({ 'to' => $LJ::ACCOUNTS_EMAIL, 'from' => $LJ::ACCOUNTS_EMAIL, 'wrap' => 1, 'subject' => "CC Payment Failure Notification: Payment #$cartobj->{'payid'}", 'body' => "This warning has been sent because the following AVS failure " . "has occurred on $LJ::SITENAMESHORT.\n\n" . " Payid: $cartobj->{'payid'}\n" . " Time: " . LJ::mysql_time() . "\n\n" . "Below are failure stats for each user listed on this cart.\n\n" . $emailtxt, }) if $emailtxt; } if ($err) { $unlock->(); return "Error processing payment: $err" . "

It is possible that your bank has placed a temporary hold on the funds ". "for this authorization, but they should be released after a short ". "period of time. Your card has not been charged.

" . "

For more information on AVS and credit card transactions, " . "" . "please see the FAQ.

"; } # if the order amount is 0, mark this cart as free. this catches # coppa-only orders and orders containing coppa which needed to # continue to the cc.bml page if ($cartobj->{amount} <= 0) { $dbh->do("UPDATE payments SET used='N', mailed='N', ". "method='free', daterecv=NOW() WHERE payid=?", undef, $cartobj->{'payid'}); } # if the order was processed successfully, we'll mark the user with allow_pay LJ::Pay::note_payment_from_user($u); # $success->() will call $unlock->() for us return $success->(); } _code?> <=body page?>