Files
HotLunches/ppPaymentHotLunch.php
2026-10-19 15:22:52 -07:00

297 lines
10 KiB
PHP

<?php
// Buffer anything db.php prints so a later HTTP 500 still reaches PayPal.
ob_start();
$noSession = 1;
require("db.php");
date_default_timezone_set("Canada/Pacific");
if (!is_array($_POST) || count($_POST) == 0) {
$contents = file_get_contents("/m/data1/app.hotlunches.net/ppPaymentHotLunch.log");
echo(nl2br(htmlentities($contents)));
exit();
}
function saveOutput($contents) {
$f = fopen("/m/data1/app.hotlunches.net/ppPaymentHotLunch.log", "w");
fwrite($f, $contents);
fclose($f);
$f = fopen("/m/data1/app.hotlunches.net/ppPaymentHotLunch.trx.log", "a");
fwrite($f, $contents);
fclose($f);
return nl2br(htmlentities($contents));
}
function hotlunches_paypal_extract_status($body) {
$body = trim($body);
if ($body === 'VERIFIED' || $body === 'INVALID') return $body;
if (preg_match('/(?:^|\r\n|\n)(VERIFIED|INVALID)\s*$/', $body, $m)) return $m[1];
return $body;
}
function hotlunches_paypal_postback_curl($url, $req) {
if (!function_exists('curl_init')) {
return array('ok' => false, 'status' => '', 'error' => 'cURL is not available');
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSLVERSION, 6); // TLS 1.2
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
if (defined('CURL_IPRESOLVE_V4')) {
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'User-Agent: PHP-IPN-VerificationScript',
'Connection: Close'
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
$errstr = curl_error($ch);
$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno) {
return array('ok' => false, 'status' => '', 'error' => $errno . ' - ' . $errstr);
}
if ($http_code !== 200) {
return array('ok' => false, 'status' => hotlunches_paypal_extract_status($body), 'error' => 'PayPal HTTP ' . $http_code);
}
return array('ok' => true, 'status' => hotlunches_paypal_extract_status($body), 'error' => '');
}
function hotlunches_paypal_postback_socket($url, $req) {
$url_parts = parse_url($url);
$host = isset($url_parts['host']) ? $url_parts['host'] : '';
$path = isset($url_parts['path']) && $url_parts['path'] !== '' ? $url_parts['path'] : '/cgi-bin/webscr';
if ($host === '') {
return array('ok' => false, 'status' => '', 'error' => 'Invalid PayPal URL');
}
$ssl = array(
'verify_peer' => true,
'verify_peer_name' => true,
'peer_name' => $host
);
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
$ssl['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
}
$ctx = stream_context_create(array('ssl' => $ssl));
$fp = @stream_socket_client('ssl://' . $host . ':443', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
if ($fp === false) {
return array('ok' => false, 'status' => '', 'error' => $errno . ' - ' . $errstr);
}
// HTTP/1.0 avoids chunked encoding so the body is exactly VERIFIED/INVALID
$header = "POST " . $path . " HTTP/1.0\r\n";
$header .= "Host: " . $host . "\r\n";
$header .= "User-Agent: PHP-IPN-VerificationScript\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n";
$header .= "Connection: close\r\n\r\n";
fwrite($fp, $header . $req);
$response = '';
while (!feof($fp)) {
$response .= fgets($fp, 1024);
}
fclose($fp);
$header_body = preg_split("/\r\n\r\n|\n\n/", $response, 2);
$status = isset($header_body[1]) ? hotlunches_paypal_extract_status($header_body[1]) : '';
if ($status === 'VERIFIED' || $status === 'INVALID') {
return array('ok' => true, 'status' => $status, 'error' => '');
}
return array('ok' => false, 'status' => $status, 'error' => 'Unexpected socket response from PayPal');
}
// Rebuild the original IPN payload. PayPal requires original field order/encoding.
$raw_post_data = file_get_contents('php://input');
if ($raw_post_data === false || $raw_post_data === '') {
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value) {
$req .= '&' . $key . '=' . urlencode(stripslashes($value));
}
} else {
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
$keyval = explode('=', $keyval, 2);
if (count($keyval) == 2) {
if ($keyval[0] === 'payment_date' && substr_count($keyval[1], '+') === 1) {
$keyval[1] = str_replace('+', '%2B', $keyval[1]);
}
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
}
$req = 'cmd=_notify-validate';
foreach ($myPost as $key => $value) {
$req .= '&' . $key . '=' . urlencode($value);
}
}
$use_sandbox = array_key_exists('test_ipn', $_POST);
$paypal_urls = $use_sandbox
? array(
'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr',
'https://www.sandbox.paypal.com/cgi-bin/webscr'
)
: array(
'https://ipnpb.paypal.com/cgi-bin/webscr',
'https://www.paypal.com/cgi-bin/webscr'
);
$errors = array();
$unexpectedErrors = 0;
$paypal_status = '';
$paypal_error = '';
$verified = false;
foreach ($paypal_urls as $url_index => $paypal_url) {
if ($verified || $paypal_status === 'INVALID') break;
// Primary endpoint is retried once. The backup endpoint is tried once.
// Timeouts stay inside PayPal's IPN response window.
$max_attempts = ($url_index === 0) ? 2 : 1;
for ($attempt = 1; $attempt <= $max_attempts; $attempt++) {
$result = hotlunches_paypal_postback_curl($paypal_url, $req);
$paypal_status = $result['status'];
$paypal_error = $result['error'];
if ($result['ok'] && ($paypal_status === 'VERIFIED' || $paypal_status === 'INVALID')) {
$verified = ($paypal_status === 'VERIFIED');
break;
}
}
// Socket fallback only when cURL never got a final VERIFIED/INVALID answer.
if ($verified || $paypal_status === 'INVALID' || $url_index !== 0) continue;
$result = hotlunches_paypal_postback_socket($paypal_url, $req);
$paypal_status = $result['status'];
$paypal_error = $result['error'];
if ($result['ok'] && $paypal_status === 'VERIFIED') {
$verified = true;
}
}
// Transport failure must be HTTP 500 so PayPal resends the IPN.
// INVALID stays HTTP 200 so a rejected notice is not retried.
if (!$verified && $paypal_status !== 'INVALID') {
if (function_exists('http_response_code')) {
http_response_code(500);
} else {
header('HTTP/1.1 500 Internal Server Error');
}
}
ob_start("saveOutput");
echo(date('r') . "\n\n");
$x = $_POST;
if (isset($x['callbackPW'])) unset($x['callbackPW']);
print_r($x);
echo("\n\n");
if ($verified) {
echo("VERIFIED");
} elseif ($paypal_status === 'INVALID') {
echo("INVALID");
$errors[] = "The PayPal server could not validate the IPN.";
$unexpectedErrors = 1;
} else {
echo("ERROR\n" . $paypal_error);
$errors[] = "Error connecting to PayPal server to validate IPN.";
$unexpectedErrors = 1;
}
echo("\n\n");
if ($unexpectedErrors == 1) exit();
extract($_POST,EXTR_PREFIX_ALL,'P');
$school_num = isset($P_custom) ? intval($P_custom) : 0;
// Load user information from the database
$query = "SELECT * FROM schools WHERE record_num = $school_num";
$r = db_query("SELECT * FROM schools WHERE record_num = {$school_num}");
if (db_num_rows($r) != 1) {
echo("Invalid school number: $school_num.\n");
echo("$query");
exit();
} else {
extract(db_fetch_assoc($r), EXTR_PREFIX_ALL, 's');
}
// Check that the payment was made in Canadian funds
if (!$P_mc_currency || $P_mc_currency != 'CAD') {
echo("Unexpected currency - payment should be made in $s_paypal_currency");
exit();
}
// Get the invoice number(s)
$r = db_query("SELECT * FROM invoice_header WHERE status = 'Open' AND school_num = $school_num");
if (db_num_rows($r) == 0) {
echo("Error: no open invoices found.\n");
echo("$query");
exit();
}
$invoices = "";
while ($row = db_fetch_array($r)) {
if (strlen($invoices)) $invoices .= ",";
$invoices .= $row['record_num'];
}
// Get current outstanding balance
$current_balance_owing = db_getVal("select sum(total - amount_paid) from invoice_header where school_num = $school_num and record_num in ($invoices)");
// Check the order total
if (!isset($P_mc_gross) || $P_mc_gross != $current_balance_owing) {
$x = number_format($current_balance_owing, 2);
echo("Invalid payment amount - expected {$x}");
exit();
}
// Check the payment was sent to the correct account
if ($P_receiver_email != 'mark@hotlunches.net' && $P_receiver_email != 'paypal@hotlunches.net') {
echo("Payment was sent to an unexpected account.\n");
echo("$P_receiver_email => 'mark@hotlunches.net'\n");
exit();
}
// check for non-completed orders (ie. pending for some reason)
if ($P_payment_status != 'Completed') {
echo("Payment was not completed.");
exit();
}
echo("Payment validated ok - updating database\n\n");
// Update invoice header
$today = date("Y-m-d",time());
$query = "UPDATE invoice_header " .
"SET amount_paid = total, status = 'Paid', reference = '{$P_txn_id}', payment_date = '$today', payment_method = 'PayPal' " .
"WHERE school_num = $school_num " .
"AND record_num in ($invoices)";
db_query($query);
echo("{$query}\n");
// Update expiry date
$renewal_date = db_getVal("SELECT max(renewal_date) FROM invoice_header WHERE school_num = $school_num");
$edate = date("Y-m-d",strtotime($renewal_date) - 60 * 60 * 12);
$query = "UPDATE schools SET expiry_date = '$edate' WHERE record_num = $school_num";
db_query($query);
echo("{$query}\n");
echo("Payment processed ok\n\n\n");
?>