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"); ?>