PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.17.0
GiveWP – Donation Plugin and Fundraising Platform v4.17.0
4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 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 All 256 releases
← All changes | src/PaymentGateways/Gateways/PayPalStandard/Controllers/PayPalStandardWebhook.php +199 -0 4.16.3 → 4.17.0 View file →
@@ -1,8 +1,10 @@
1 1 <?php
2 2
3 3 namespace Give\PaymentGateways\Gateways\PayPalStandard\Controllers;
4 4
5 +use Give\Donations\Models\Donation;
6 +use Give\Framework\Support\ValueObjects\Money;
5 7 use Give\Log\Log;
6 8 use Give\PaymentGateways\Gateways\PayPalStandard\PayPalStandard;
7 9 use Give\PaymentGateways\Gateways\PayPalStandard\Webhooks\WebhookRegister;
8 10 use Give\PaymentGateways\Gateways\PayPalStandard\Webhooks\WebhookValidator;
@@ -29,8 +31,9 @@
29 31 * Handle PayPal ipn
30 32 *
31 33 * @since 2.19.0
32 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.
33 36 */
34 37 public function handle()
35 38 {
36 39 $eventData = file_get_contents('php://input');
@@ -55,8 +58,12 @@
55 58 );
56 59 exit();
57 60 }
58 61
62 + if ( ! $this->verifyEventData($eventData, $donationId, $txnType)) {
63 + exit();
64 + }
65 +
59 66 $this->recordIpn($eventData, $donationId);
60 67 $this->recordIpnInDonation($donationId);
61 68
62 69 /* @var WebhookRegister $webhookRegisterer */
@@ -156,6 +163,198 @@
156 163 * @param int $donationId donation id.
157 164 */
158 165 do_action('give_paypal_web_accept', $eventData, $donationId);
159 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;
160 359 }
161 360 }