1<?php
2 // Require the functions to fetch config values
3 require 'config.php';
4
5 $pagseguro = $config['pagseguro'];
6 $notificationCode = $_POST['notificationCode'] ?? null;
7 $notificationType = $_POST['notificationType'] ?? null;
8
9 // Require the functions to connect to database
10 require 'engine/database/connect.php';
11
12 // Fetch and sanitize POST and GET values
13 function getValue($value) {
14 return (!empty($value)) ? sanitize($value) : false;
15 }
16 function sanitize($data) {
17 return htmlentities(strip_tags(mysql_znote_escape_string($data)));
18 }
19
20 require_once 'engine/function/translate.php';
21 require_once 'engine/function/settings.php';
22 require_once 'engine/function/users.php';
23 require_once 'engine/function/plugins.php';
24 znote_apply_settings();
25 znote_plugins_load();
26
27 // Util function to insert log
28 function report($code, $details = '') {
29 $connectedIp = $_SERVER['REMOTE_ADDR'];
30 $details = getValue($details);
31 $details .= '\nConnection from IP: '. $connectedIp;
32 db()->execute('INSERT INTO `znote_pagseguro_notifications` VALUES (null, ?, ?, CURRENT_TIMESTAMP)', [getValue($code), $details]);
33 }
34
35 function VerifyPagseguroIPN($code) {
36 global $pagseguro;
37 $url = $pagseguro['urls']['ws'];
38
39 $cURL = curl_init();
40 curl_setopt($cURL, CURLOPT_SSL_VERIFYPEER, false);
41 curl_setopt($cURL, CURLOPT_SSL_VERIFYHOST, false);
42 curl_setopt($cURL, CURLOPT_URL, 'https://' . $url . '/v3/transactions/notifications/' . $code . '?email=' . $pagseguro['email'] . '&token=' . $pagseguro['token']);
43 curl_setopt($cURL, CURLOPT_HEADER, false);
44 curl_setopt($cURL, CURLOPT_RETURNTRANSFER, true);
45 curl_setopt($cURL, CURLOPT_FORBID_REUSE, true);
46 curl_setopt($cURL, CURLOPT_FRESH_CONNECT, true);
47 curl_setopt($cURL, CURLOPT_CONNECTTIMEOUT, 30);
48 curl_setopt($cURL, CURLOPT_TIMEOUT, 60);
49 curl_setopt($cURL, CURLINFO_HEADER_OUT, true);
50 curl_setopt($cURL, CURLOPT_HTTPHEADER, array(
51 'Connection: close',
52 'Expect: ',
53 ));
54 $Response = curl_exec($cURL);
55 $Status = (int)curl_getinfo($cURL, CURLINFO_HTTP_CODE);
56 curl_close($cURL);
57
58 $output = print_r($Response, true);
59 if(empty($Response) OR !$Status){
60 return null;
61 }
62 if(intval($Status / 100) != 2){
63 return false;
64 }
65 return trim($Response);
66 }
67
68 // Send an empty HTTP 200 OK response to acknowledge receipt of the notification
69 header('HTTP/1.1 200 OK');
70
71 if(empty($notificationCode) || empty($notificationType)){
72 report($notificationCode, 'notificationCode or notificationType is empty. Type: ' . $notificationType . ', Code: ' . $notificationCode);
73 exit();
74 }
75
76 if ($notificationType !== 'transaction') {
77 report($notificationCode, 'Unknown ' . $notificationType . ' notificationType');
78 exit();
79 }
80
81 $rawPayment = VerifyPagseguroIPN($notificationCode);
82 $payment = simplexml_load_string((string)$rawPayment);
83 if ($payment === false) {
84 die('Error: invalid PagSeguro response.');
85 }
86 $paymentStatus = (int) $payment->status;
87 $paymentCode = sanitize($payment->code);
88
89 report($notificationCode, $rawPayment);
90
91 // Updating Payment Status
92 db()->execute('UPDATE `znote_pagseguro` SET `payment_status` = ? WHERE `transaction` = ?', [$paymentStatus, $paymentCode]);
93
94 // Check that the payment_status is Completed
95 if ($paymentStatus == 3) {
96
97 // Check that transaction has not been previously processed
98 $transaction = db()->fetchOne('SELECT `transaction`, `completed` FROM `znote_pagseguro` WHERE `transaction` = ?', [$paymentCode]);
99 $status = true;
100 $customRaw = (string)$payment->reference;
101 $custom = (int)$customRaw;
102
103 if (!is_array($transaction) || $transaction['completed'] == '1') {
104 $status = false;
105 }
106
107 if ($payment->grossAmount == 0.0) $status = false; // Wrong ammount of money
108 $item = $payment->items->item[0];
109 $paymentData = array(
110 'provider' => 'pagseguro',
111 'reference' => (string)$paymentCode,
112 'custom' => $customRaw,
113 'account_id' => $custom,
114 'price' => (float)$item->amount,
115 'currency' => $pagseguro['currency'] ?? '',
116 'points' => (int)$item->quantity,
117 'status' => 'completed',
118 'raw' => $rawPayment,
119 'resolved' => false,
120 );
121 if (function_exists('znote_hook_filter')) {
122 $paymentData = znote_hook_filter('payment.resolve', $paymentData, array('provider' => 'pagseguro', 'raw' => $rawPayment));
123 }
124 if (!empty($paymentData['resolved'])) {
125 $custom = (int)($paymentData['account_id'] ?? 0);
126 } elseif ($item->amount != ($pagseguro['price'] / 100)) $status = false;
127 if (number_format((float)$item->amount, 2, '.', '') !== number_format((float)($paymentData['price'] ?? 0), 2, '.', '')) $status = false;
128 if ($custom <= 0) $status = false;
129
130 if ($status) {
131 $paidPoints = (int)($paymentData['points'] ?? $item->quantity);
132
133 // Re-check completion status and credit inside one locked transaction,
134 // so two concurrent notifications for the same transaction cannot both credit points.
135 $creditResult = db()->transaction(function ($db) use ($paymentCode, $custom, $paidPoints) {
136 $row = $db->fetchOne('SELECT `completed` FROM `znote_pagseguro` WHERE `transaction` = ? LIMIT 1 FOR UPDATE;', [$paymentCode]);
137 if (!is_array($row) || (int)$row['completed'] === 1) {
138 return 'duplicate';
139 }
140
141 $db->execute('UPDATE `znote_pagseguro` SET `completed` = 1 WHERE `transaction` = ?', [$paymentCode]);
142
143 $data = $db->fetchOne("SELECT `points` AS `old_points` FROM `znote_accounts` WHERE `account_id` = ? LIMIT 1 FOR UPDATE;", [$custom]);
144 if (!is_array($data)) {
145 return 'no_account';
146 }
147
148 $new_points = (int)$data['old_points'] + $paidPoints;
149 $db->execute("UPDATE `znote_accounts` SET `points` = ? WHERE `account_id` = ?", [$new_points, $custom]);
150
151 return 'credited';
152 });
153
154 if ($creditResult === 'credited') {
155 if (function_exists('znote_hook')) {
156 znote_hook('payment.completed', array_merge($paymentData, array(
157 'provider' => 'pagseguro',
158 'reference' => (string)$paymentCode,
159 'custom' => $customRaw,
160 'account_id' => $custom,
161 'price' => $paymentData['price'] ?? (float)$item->amount,
162 'currency' => $paymentData['currency'] ?? ($pagseguro['currency'] ?? ''),
163 'points' => $paidPoints,
164 'status' => 'completed',
165 )));
166 }
167 } elseif ($creditResult === 'no_account') {
168 report($notificationCode, 'No znote_accounts row for account_id ' . $custom);
169 }
170 }
171 } else if ($paymentStatus == 7) {
172 db()->execute('UPDATE `znote_pagseguro` SET `completed` = 1 WHERE `transaction` = ?', [$paymentCode]);
173 }
174?>
175