PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.16.6.1
GiveWP – Donation Plugin and Fundraising Platform v4.16.6.1
4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / PaymentGateways / Gateways / PayPalStandard / Controllers / PayPalStandardWebhook.php
give / src / PaymentGateways / Gateways / PayPalStandard / Controllers Last commit date
PayPalStandardWebhook.php 2 days ago
PayPalStandardWebhook.php
361 lines
1 <?php
2
3 namespace Give\PaymentGateways\Gateways\PayPalStandard\Controllers;
4
5 use Give\Donations\Models\Donation;
6 use Give\Framework\Support\ValueObjects\Money;
7 use Give\Log\Log;
8 use Give\PaymentGateways\Gateways\PayPalStandard\PayPalStandard;
9 use Give\PaymentGateways\Gateways\PayPalStandard\Webhooks\WebhookRegister;
10 use Give\PaymentGateways\Gateways\PayPalStandard\Webhooks\WebhookValidator;
11
12 /**
13 * This class use to handle PayPal ipn.
14 *
15 * @since 2.19.0
16 */
17 class PayPalStandardWebhook
18 {
19
20 /**
21 * @var WebhookValidator
22 */
23 private $webhookValidator;
24
25 public function __construct(WebhookValidator $webhookValidator)
26 {
27 $this->webhookValidator = $webhookValidator;
28 }
29
30 /**
31 * Handle PayPal ipn
32 *
33 * @since 2.19.0
34 * @since 2.19.3 Respond with 200 http status to ipn.
35 * @since 4.16.6.1 Add IPN event-data validation before processing.
36 */
37 public function handle()
38 {
39 $eventData = file_get_contents('php://input');
40 $eventData = wp_parse_args($eventData);
41
42 if ( ! $this->webhookValidator->verifyEventSignature($eventData)) {
43 exit();
44 }
45
46 $donationId = isset($eventData['custom']) ? absint($eventData['custom']) : 0;
47 $txnType = $eventData['txn_type'];
48
49 // ipn verification can be disabled in GiveWP (<=2.15.0).
50 // This check will prevent anonymous requests from editing donation, if ipn verification disabled.
51 if ( ! $this->verifyDonationId($donationId)) {
52 Log::error(
53 'PayPal Standard IPN Error',
54 [
55 'Message' => 'Donation id (from IPN) does not exist.',
56 'Event Data' => $eventData,
57 ]
58 );
59 exit();
60 }
61
62 if ( ! $this->verifyEventData($eventData, $donationId, $txnType)) {
63 exit();
64 }
65
66 $this->recordIpn($eventData, $donationId);
67 $this->recordIpnInDonation($donationId);
68
69 /* @var WebhookRegister $webhookRegisterer */
70 $webhookRegisterer = give(WebhookRegister::class);
71 if ($webhookRegisterer->hasEventRegistered($txnType)) {
72 $webhookRegisterer->getEventHandler($txnType)->processEvent((object)$eventData);
73 }
74
75 $this->supportLegacyActions($txnType, $eventData, $donationId);
76
77 exit;
78 }
79
80 /**
81 * @since 2.19.0
82 *
83 * @param int $donationId
84 *
85 * @param array $eventData
86 */
87 private function recordIpn(array $eventData, $donationId)
88 {
89 update_option(
90 'give_last_paypal_ipn_received',
91 [
92 'auth_status' => 'VERIFIED',
93 'transaction_id' => isset($eventData['txn_id']) ? $eventData['txn_id'] : 'N/A',
94 'payment_id' => $donationId,
95 ],
96 false
97 );
98 }
99
100 /**
101 * @since 2.19.0
102 *
103 * @param int $donationId
104 */
105 private function recordIpnInDonation($donationId)
106 {
107 $currentTimestamp = current_time('timestamp');
108
109 give_insert_payment_note(
110 $donationId,
111 sprintf(
112 __('IPN received on %1$s at %2$s', 'give'),
113 date_i18n('m/d/Y', $currentTimestamp),
114 date_i18n('H:i', $currentTimestamp)
115 )
116 );
117
118 give_update_meta($donationId, 'give_last_paypal_ipn_received', $currentTimestamp);
119 }
120
121 /**
122 * @param $donationId
123 *
124 * @return bool
125 */
126 private function verifyDonationId($donationId)
127 {
128 return $donationId && PayPalStandard::id() === give_get_payment_gateway($donationId);
129 }
130
131 /**
132 * @since 2.19.0
133 *
134 * @param string $txnType
135 * @param array $eventData
136 * @param int $donationId
137 *
138 * @return void
139 */
140 private function supportLegacyActions($txnType, array $eventData, $donationId)
141 {
142 if (has_action('give_paypal_' . $txnType)) {
143 /**
144 * Fires while processing PayPal IPN $txnType.
145 *
146 * Allow PayPal IPN types to be processed separately.
147 *
148 * @since 1.0
149 *
150 * @param array $eventData Encoded data.
151 * @param int $donationId donation id.
152 */
153 do_action("give_paypal_{$txnType}", $eventData, $donationId);
154 } else {
155 /**
156 * Fires while process PayPal IPN.
157 *
158 * Fallback to web accept just in case the txn_type isn't present.
159 *
160 * @since 1.0
161 *
162 * @param array $eventData Encoded data.
163 * @param int $donationId donation id.
164 */
165 do_action('give_paypal_web_accept', $eventData, $donationId);
166 }
167 }
168
169 /**
170 * @since 4.16.6.1
171 */
172 private function verifyEventData(array $eventData, int $donationId, $txnType): bool
173 {
174 $paymentStatus = strtolower($eventData['payment_status'] ?? '');
175
176 if ( ! $this->verifyReceiverEmail($eventData)) {
177 return false;
178 }
179
180 if (in_array($paymentStatus, ['completed', 'pending'], true)) {
181 if ( ! $this->verifyPaymentAmount($eventData, $donationId)) {
182 return false;
183 }
184 }
185
186 if (in_array($paymentStatus, ['refunded', 'reversed'], true)) {
187 if ( ! $this->verifyParentTransactionId($eventData, $donationId)) {
188 return false;
189 }
190 }
191
192 return true;
193 }
194
195 /**
196 * @since 4.16.6.1
197 */
198 private function verifyReceiverEmail(array $eventData)
199 {
200 $sitePaypalEmail = trim((string) give_get_option('paypal_email', ''));
201 if ($sitePaypalEmail === '') {
202 return true;
203 }
204
205 $receiverEmail = strtolower(trim((string) ($eventData['receiver_email'] ?? '')));
206 $business = strtolower(trim((string) ($eventData['business'] ?? '')));
207 $siteEmail = strtolower($sitePaypalEmail);
208
209 if ($receiverEmail === '' && $business === '') {
210 return true;
211 }
212
213 if ($receiverEmail !== $siteEmail && $business !== $siteEmail) {
214 Log::error(
215 'PayPal Standard IPN Error',
216 [
217 'Message' => sprintf(
218 'IPN receiver_email (%s) / business (%s) does not match the site PayPal email (%s).',
219 $eventData['receiver_email'] ?? '(not set)',
220 $eventData['business'] ?? '(not set)',
221 $sitePaypalEmail
222 ),
223 'Event Data' => $eventData,
224 ]
225 );
226
227 return false;
228 }
229
230 return true;
231 }
232
233 /**
234 * @since 4.16.6.1
235 */
236 private function verifyPaymentAmount(array $eventData, $donationId)
237 {
238 try {
239 $donation = Donation::find($donationId);
240
241 if ( ! $donation) {
242 Log::error(
243 'PayPal Standard IPN Error',
244 [
245 'Message' => sprintf(
246 'Donation #%d not found.',
247 $donationId
248 ),
249 'Event Data' => $eventData,
250 ]
251 );
252
253 return false;
254 }
255
256 $currency = strtoupper(trim((string) ($eventData['mc_currency'] ?? '')));
257 $donationCurrency = strtoupper(trim($donation->amount->getCurrency()->getCode()));
258
259 if ($currency !== $donationCurrency) {
260 Log::error(
261 'PayPal Standard IPN Error',
262 [
263 'Message' => sprintf(
264 'IPN currency (%s) does not match donation #%d currency (%s).',
265 $currency,
266 $donationId,
267 $donationCurrency
268 ),
269 'Event Data' => $eventData,
270 ]
271 );
272
273 return false;
274 }
275
276 $ipnAmount = Money::fromDecimal((float)($eventData['mc_gross'] ?? 0), $currency);
277
278 if ( ! $ipnAmount->equals($donation->intendedAmount())) {
279 Log::error(
280 'PayPal Standard IPN Error',
281 [
282 'Message' => sprintf(
283 'IPN amount (%s %s) does not match donation #%d amount (%s %s).',
284 $eventData['mc_gross'] ?? '0',
285 $currency,
286 $donationId,
287 $donation->intendedAmount()->formatToDecimal(),
288 $donationCurrency
289 ),
290 'Event Data' => $eventData,
291 ]
292 );
293
294 return false;
295 }
296 } catch (\Exception $e) {
297 Log::error(
298 'PayPal Standard IPN Error',
299 [
300 'Message' => 'Failed to compare IPN amount to donation amount.',
301 'Exception' => $e->getMessage(),
302 'Event Data' => $eventData,
303 ]
304 );
305
306 return false;
307 }
308
309 return true;
310 }
311
312 /**
313 * @since 4.16.6.1
314 */
315 private function verifyParentTransactionId(array $eventData, $donationId)
316 {
317 $parentTxnId = trim((string) ($eventData['parent_txn_id'] ?? ''));
318 if ($parentTxnId === '') {
319 return true;
320 }
321
322 $donation = Donation::find($donationId);
323 $storedTxnId = $donation ? trim((string) $donation->gatewayTransactionId) : '';
324
325 if ($storedTxnId === '') {
326 Log::error(
327 'PayPal Standard IPN Error',
328 [
329 'Message' => sprintf(
330 'IPN payment_status is %s but donation #%d has no stored transaction ID — cannot process a refund for a donation that was never completed.',
331 strtolower($eventData['payment_status'] ?? ''),
332 $donationId
333 ),
334 'Event Data' => $eventData,
335 ]
336 );
337
338 return false;
339 }
340
341 if ($parentTxnId !== $storedTxnId) {
342 Log::error(
343 'PayPal Standard IPN Error',
344 [
345 'Message' => sprintf(
346 'IPN parent_txn_id (%s) does not match donation #%d stored transaction ID (%s).',
347 $parentTxnId,
348 $donationId,
349 $storedTxnId
350 ),
351 'Event Data' => $eventData,
352 ]
353 );
354
355 return false;
356 }
357
358 return true;
359 }
360 }
361