ipn.php

main 229 lines · 7.9 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/* 2021: Paypal hosts arent neccesarily notify.paypal.com any longer.
3if (gethostbyaddr($_SERVER['REMOTE_ADDR']) !== 'notify.paypal.com') {
4 exit();
5}
6*/
7
8function ip_in_range( $ip, $range ) {
9 if ( strpos( $range, '/' ) === false ) {
10 $range .= '/32';
11 }
12 // $range is in IP/CIDR format eg 127.0.0.1/24
13 list( $range, $netmask ) = explode( '/', $range, 2 );
14 $range_decimal = ip2long( $range );
15 $ip_decimal = ip2long( $ip );
16 $wildcard_decimal = pow( 2, ( 32 - (int)$netmask ) ) - 1;
17 $netmask_decimal = ~ $wildcard_decimal;
18 return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
19}
20
21$paypal_ip_ranges = array(
22 "173.0.81.65",
23 "173.0.81.140",
24 "64.4.240.0/21",
25 "64.4.248.0/22",
26 "66.211.168.0/22",
27 "173.0.80.0/20",
28 "91.243.72.0/23"
29);
30
31$verified = false;
32for($i = 0; $i < count($paypal_ip_ranges); $i++) {
33 if(ip_in_range($_SERVER["REMOTE_ADDR"], $paypal_ip_ranges[$i])) {
34 $verified = true;
35 break;
36 }
37}
38
39if(!$verified) {
40 exit();
41}
42
43// Require the functions to connect to database and fetch config values
44require 'config.php';
45require 'engine/database/connect.php';
46
47// Fetch and sanitize POST and GET values
48function getValue($value) {
49 return (!empty($value)) ? sanitize($value) : false;
50}
51function sanitize($data) {
52 return htmlentities(strip_tags(mysql_znote_escape_string($data)));
53}
54
55require_once 'engine/function/translate.php';
56require_once 'engine/function/settings.php';
57require_once 'engine/function/users.php';
58require_once 'engine/function/plugins.php';
59znote_apply_settings();
60znote_plugins_load();
61
62function VerifyPaypalIPN(?array $IPN = null){
63 if(empty($IPN)){
64 $IPN = $_POST;
65 }
66 if(empty($IPN['verify_sign'])){
67 return null;
68 }
69 $IPN['cmd'] = '_notify-validate';
70 $PaypalHost = (empty($IPN['test_ipn']) ? 'www' : 'www.sandbox').'.paypal.com';
71 $cURL = curl_init();
72 curl_setopt($cURL, CURLOPT_SSL_VERIFYPEER, 1);
73 curl_setopt($cURL, CURLOPT_SSL_VERIFYHOST, 2);
74 curl_setopt($cURL, CURLOPT_SSLVERSION, 6);
75 curl_setopt($cURL, CURLOPT_CAINFO, __DIR__ . '/engine/cert/cacert.pem');
76 curl_setopt($cURL, CURLOPT_URL, "https://{$PaypalHost}/cgi-bin/webscr");
77 curl_setopt($cURL, CURLOPT_ENCODING, 'gzip');
78 curl_setopt($cURL, CURLOPT_POST, true); // POST back
79 curl_setopt($cURL, CURLOPT_POSTFIELDS, $IPN); // the $IPN
80 curl_setopt($cURL, CURLOPT_HEADER, false);
81 curl_setopt($cURL, CURLOPT_RETURNTRANSFER, true);
82 curl_setopt($cURL, CURLOPT_FORBID_REUSE, true);
83 curl_setopt($cURL, CURLOPT_FRESH_CONNECT, true);
84 curl_setopt($cURL, CURLOPT_CONNECTTIMEOUT, 30);
85 curl_setopt($cURL, CURLOPT_TIMEOUT, 60);
86 curl_setopt($cURL, CURLINFO_HEADER_OUT, true);
87 curl_setopt($cURL, CURLOPT_HTTPHEADER, array(
88 'Connection: close',
89 'Expect: ',
90 ));
91 $Response = curl_exec($cURL);
92 $Status = (int)curl_getinfo($cURL, CURLINFO_HTTP_CODE);
93 curl_close($cURL);
94 if(empty($Response) or !preg_match('~^(VERIFIED|INVALID)$~i', $Response = trim($Response)) or !$Status){
95 return null;
96 }
97 if(intval($Status / 100) != 2){
98 return false;
99 }
100 return !strcasecmp($Response, 'VERIFIED');
101}
102
103// Fetch paypal configurations
104$paypal = $config['paypal'];
105$prices = $config['paypal_prices'];
106
107// Send an empty HTTP 204 OK response to acknowledge receipt of the notification
108http_response_code(204);
109
110// Build the required acknowledgement message out of the notification just received
111$postdata = 'cmd=_notify-validate';
112if(!empty($_POST)){
113 $postdata.="&".http_build_query($_POST);
114}
115// Assign payment notification values to local variables
116$item_name = $_POST['item_name'] ?? null;
117$item_number = $_POST['item_number'] ?? null;
118$payment_status = $_POST['payment_status'] ?? null;
119$payment_amount = $_POST['mc_gross'] ?? null;
120$payment_currency = $_POST['mc_currency'] ?? null;
121$txn_id = getValue($_POST['txn_id'] ?? null);
122$receiver_email = getValue($_POST['receiver_email'] ?? null);
123$payer_email = getValue($_POST['payer_email'] ?? null);
124$custom_raw = (string)($_POST['custom'] ?? '');
125$custom = (int)$custom_raw;
126
127$connectedIp = $_SERVER['REMOTE_ADDR'];
128db()->execute("INSERT INTO `znote_paypal` VALUES ('0', '0', ?, '0', '0', '0')", ["Connection from IP: $connectedIp"]);
129
130$status = VerifyPaypalIPN();
131if ($status) {
132 // Check that the payment_status is Completed
133 if ($payment_status == 'Completed') {
134
135
136 // Check that txn_id has not been previously processed
137 $txn_id_check = db()->fetchOne("SELECT `txn_id` FROM `znote_paypal` WHERE `txn_id` = ?", [$txn_id]);
138 if ($txn_id_check === false) {
139 // Check that receiver_email is your Primary PayPal email
140 if ($receiver_email == $paypal['email']) {
141
142 $status = true;
143 $paidMoney = 0;
144 $paidPoints = 0;
145 $payment = array(
146 'provider' => 'paypal',
147 'reference' => (string)$txn_id,
148 'custom' => $custom_raw,
149 'account_id' => $custom,
150 'price' => $payment_amount,
151 'currency' => $payment_currency,
152 'points' => 0,
153 'status' => 'Completed',
154 'raw' => $_POST,
155 'resolved' => false,
156 );
157 if (function_exists('znote_hook_filter')) {
158 $payment = znote_hook_filter('payment.resolve', $payment, array('provider' => 'paypal', 'raw' => $_POST));
159 }
160
161 if (!empty($payment['resolved'])) {
162 $custom = (int)($payment['account_id'] ?? 0);
163 $paidMoney = $payment['price'] ?? 0;
164 $paidPoints = (int)($payment['points'] ?? 0);
165 } else {
166 foreach ($prices as $priceValue => $pointsValue) {
167 if ($priceValue == $payment_amount) {
168 $paidMoney = $priceValue;
169 $paidPoints = $pointsValue;
170 }
171 }
172 }
173
174 if ($paidMoney == 0 || number_format((float)$paidMoney, 2, '.', '') !== number_format((float)$payment_amount, 2, '.', '')) $status = false; // Wrong ammount of money
175 if ($payment_currency != ($payment['currency'] ?? $paypal['currency'])) $status = false; // Wrong currency
176 if ($custom <= 0) $status = false;
177
178 // Verify that the user havent messed around with POST data
179 if ($status) {
180 // Re-check for a duplicate and credit inside one locked transaction,
181 // so two concurrent IPN deliveries for the same txn_id cannot both credit points.
182 $creditResult = db()->transaction(function ($db) use ($txn_id, $payer_email, $custom, $paidMoney, $paidPoints) {
183 $dup = $db->fetchOne("SELECT `txn_id` FROM `znote_paypal` WHERE `txn_id` = ? LIMIT 1 FOR UPDATE;", [$txn_id]);
184 if ($dup !== false) {
185 return 'duplicate';
186 }
187
188 $db->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, ?, ?, ?)", [$txn_id, $payer_email, $custom, $paidMoney, $paidPoints]);
189
190 $data = $db->fetchOne("SELECT `points` AS `old_points` FROM `znote_accounts` WHERE `account_id` = ? LIMIT 1 FOR UPDATE;", [$custom]);
191 if (!is_array($data)) {
192 return 'no_account';
193 }
194
195 $new_points = (int)$data['old_points'] + $paidPoints;
196 $db->execute("UPDATE `znote_accounts` SET `points` = ? WHERE `account_id` = ?", [$new_points, $custom]);
197
198 return 'credited';
199 });
200
201 if ($creditResult === 'credited') {
202 if (function_exists('znote_hook')) {
203 znote_hook('payment.completed', array_merge($payment, array(
204 'provider' => 'paypal',
205 'reference' => (string)$txn_id,
206 'custom' => $custom_raw,
207 'account_id' => $custom,
208 'price' => $paidMoney,
209 'currency' => $payment_currency,
210 'points' => $paidPoints,
211 'status' => 'Completed',
212 )));
213 }
214 } elseif ($creditResult === 'no_account') {
215 db()->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, '0', '0', '0')", [$txn_id, "ERROR: No znote_accounts row for account_id $custom"]);
216 }
217 }
218 } else {
219 $pmail = $paypal['email'];
220 db()->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, '0', '0', '0')", [$txn_id, "ERROR: Wrong mail. Received: $receiver_email, configured: $pmail"]);
221 }
222 }
223 }
224} else {
225 // Something is wrong
226 db()->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, '0', '0', '0')", [$txn_id, "ERROR: Invalid data. $postdata"]);
227}
228?>
229
Top