Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/paid-memberships-pro/services/braintree-webhook.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 + /**
3 + * The Braintree webhook handler
4 + *
5 + * @since 1.9.5 - Various updates to how we log & process requests from Braintree
6 + */
7 +
8 + use Braintree\WebhookNotification as Braintree_WebhookNotification;
9 +
10 + //in case the file is loaded directly
11 + if( ! defined( 'ABSPATH' ) ) {
12 + exit;
13 + }
14 +
15 + //globals
16 + global $wpdb;
17 +
18 + // Sets the PMPRO_DOING_WEBHOOK constant and fires the pmpro_doing_webhook action.
19 + pmpro_doing_webhook( 'braintree', true );
20 +
21 + // Debug log
22 + global $logstr;
23 + $logstr = array( "Logged On: " . date_i18n( "m/d/Y H:i:s", current_time( 'timestamp' ) ) );
24 + $logstr[] = "\nREQUEST:";
25 + $logstr[] = var_export( $_REQUEST, true );
26 + $logstr[] = "\n";
27 +
28 + // Don't run this with wrong PHP version
29 + if ( version_compare( PHP_VERSION, '5.4.45', '<' ) ) {
30 + return;
31 + }
32 +
33 + //load Braintree library, gateway class constructor does config
34 + if ( ! class_exists( '\Braintree' ) ) {
35 + require_once( PMPRO_DIR . "/classes/gateways/class.pmprogateway_braintree.php" );
36 + }
37 +
38 + $gateway = new PMProGateway_braintree();
39 + $webhookNotification = null;
40 +
41 + if ( empty( $_REQUEST['bt_payload'] ) ) {
42 + $logstr[] = "No payload in request?!? " . print_r( $_REQUEST, true );
43 + pmpro_braintreeWebhookExit();
44 + }
45 +
46 + if ( isset( $_POST['bt_signature'] ) && ! isset( $_POST['bt_payload'] ) ) {
47 + $logstr[] = "No payload and signature included in the request!";
48 + pmpro_braintreeWebhookExit();
49 + }
50 +
51 + //get notification
52 + try {
53 + /**
54 + * @since 1.9.5 - BUG FIX: Unable to identify Braintree Webhook messages
55 + * Expecting Braintree library to sanitize signature & payload
56 + * since using sanitize_text_field() breaks Braintree parser
57 + *
58 + * NOTE: The Braintree API needs the unsanitized input.
59 + */
60 + $webhookNotification = Braintree_WebhookNotification::parse( $_POST['bt_signature'], $_POST['bt_payload'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
61 +
62 + $logstr[] = "\webhookNotification:";
63 + $logstr[] = var_export( $webhookNotification, true );
64 + $logstr[] = "\n";
65 + } catch ( Exception $e ) {
66 + $logstr[] = "Couldn't extract notification from payload: {$_REQUEST['bt_payload']}";
67 + $logstr[] = "Error message: " . $e->getMessage();
68 +
69 + pmpro_braintreeWebhookExit();
70 + }
71 +
72 + /**
73 + * @since 1.9.5 - ENHANCEMENT: Log if notification object has unexpected format
74 + */
75 + if ( ! isset( $webhookNotification->kind ) ) {
76 + $logstr[] = "Unexpected webhook message: " . print_r( $webhookNotification, true ) . "\n";
77 + pmpro_braintreeWebhookExit();
78 + }
79 +
80 + /**
81 + * Only verifying?
82 + * @since 1.9.5 - Log Webhook tests with webhook supplied timestamp (verifies there's no caching).
83 + */
84 + if ( $webhookNotification->kind === Braintree_WebhookNotification::CHECK ) {
85 + $when = $webhookNotification->timestamp->format( 'Y-m-d H:i:s.u' );
86 +
87 + $logstr[] = "Since you are just testing the URL, check that the timestamp updates on refresh to make sure this isn't being cached.";
88 + $logstr[] = "Braintree gateway timestamp: {$when}";
89 + pmpro_braintreeWebhookExit();
90 + }
91 +
92 + //subscription charged successfully
93 + if ( $webhookNotification->kind === Braintree_WebhookNotification::SUBSCRIPTION_CHARGED_SUCCESSFULLY ) {
94 + $logstr[] = "The Braintree gateway received payment for a recurring billing plan";
95 +
96 + //need a subscription id
97 + if ( empty( $webhookNotification->subscription->id ) ) {
98 + $logstr[] = "No subscription ID.";
99 + pmpro_braintreeWebhookExit();
100 + }
101 +
102 + //figure out which order to attach to
103 + $old_order = new \MemberOrder();
104 + $old_order->getLastMemberOrderBySubscriptionTransactionID( $webhookNotification->subscription->id );
105 +
106 + //no order?
107 + if ( empty( $old_order->id ) ) {
108 + $logstr[] = "Couldn't find the original subscription with ID={$webhookNotification->subscription->id}.";
109 + pmpro_braintreeWebhookExit();
110 + }
111 +
112 + //create new order
113 + $user_id = $old_order->user_id;
114 + $user = get_userdata( $user_id );
115 +
116 + if ( empty( $user ) ) {
117 + $logstr[] = "Couldn't find the old order's user. Order ID = {$old_order->id}.";
118 + pmpro_braintreeWebhookExit();
119 + } else {
120 + $user->membership_level = pmpro_getMembershipLevelForUser( $user_id );
121 + }
122 +
123 + //data about this transaction
124 + $transaction = $webhookNotification->subscription->transactions[0];
125 +
126 + //log it for debug email
127 + $logstr[] = var_export( $transaction, true );
128 +
129 + //alright. create a new order
130 + $morder = new \MemberOrder();
131 + $morder->user_id = $old_order->user_id;
132 + $morder->membership_id = $old_order->membership_id;
133 + $morder->subtotal = $transaction->amount;
134 + $morder->total = $transaction->amount;
135 + $morder->payment_transaction_id = $transaction->id;
136 + $morder->subscription_transaction_id = $webhookNotification->subscription->id;
137 +
138 + //Assume no tax for now. Add ons will handle it later.
139 + $morder->tax = 0;
140 +
141 + $morder->gateway = $old_order->gateway;
142 + $morder->gateway_environment = $old_order->gateway_environment;
143 +
144 + $morder->billing = new stdClass();
145 +
146 + if (! empty( $transaction->billing_details) ) {
147 + $morder->billing->name = trim( $transaction->billing_details->first_name . " " . $transaction->billing_details->last_name );
148 + $morder->billing->street = $transaction->billing_details->street_address;
149 + $morder->billing->city = $transaction->billing_details->locality;
150 + $morder->billing->state = $transaction->billing_details->region;
151 + $morder->billing->zip = $transaction->billing_details->postal_code;
152 + $morder->billing->country = $transaction->billing_details->country_code_alpha2;
153 + } else {
154 + $morder->billing->name = $old_order->billing->name;
155 + $morder->billing->street = $old_order->billing->street;
156 + $morder->billing->street2 = $old_order->billing->street2;
157 + $morder->billing->city = $old_order->billing->city;
158 + $morder->billing->state = $old_order->billing->state;
159 + $morder->billing->zip = $old_order->billing->zip;
160 + $morder->billing->country = $old_order->billing->country;
161 + }
162 +
163 + $morder->billing->phone = $old_order->billing->phone;
164 +
165 + //Updates this order with the most recent orders payment method information and saves it.
166 + pmpro_update_order_with_recent_payment_method( $morder );
167 +
168 + //save
169 + $morder->status = "success";
170 + $morder->saveOrder();
171 + $morder->getMemberOrderByID( $morder->id );
172 +
173 + //email the user their order
174 + $pmproemail = new \PMProEmail();
175 + $pmproemail->sendInvoiceEmail( $user, $morder );
176 +
177 + do_action( 'pmpro_subscription_payment_completed', $morder );
178 +
179 + $logstr[] = "Triggered pmpro_subscription_payment_completed actions and returned";
180 +
181 + /**
182 + * @since 1.9.5 - Didn't terminate & save debug loggins for Webhook
183 + */
184 + pmpro_braintreeWebhookExit();
185 + }
186 +
187 + /*
188 + Note here: These next three checks all work the same way and send the same
189 + "billing failed" email, but kick off different actions based on the kind.
190 + */
191 +
192 + //subscription charged unsuccessfully
193 + if ( $webhookNotification->kind === Braintree_WebhookNotification::SUBSCRIPTION_CHARGED_UNSUCCESSFULLY ) {
194 + $logstr[] = "The Braintree gateway let us know there's a problem with the payment";
195 +
196 + //need a subscription id
197 + if ( empty( $webhookNotification->subscription->id ) ) {
198 + $logstr[] = "No subscription ID.";
199 + pmpro_braintreeWebhookExit();
200 + }
201 +
202 + //figure out which order to attach to
203 + $old_order = new \MemberOrder();
204 + $old_order->getLastMemberOrderBySubscriptionTransactionID( $webhookNotification->subscription->id );
205 +
206 + if ( empty( $old_order ) ) {
207 + $logstr[] = "Couldn't find old order for failed payment with subscription id={$webhookNotification->subscription->id}";
208 + pmpro_braintreeWebhookExit();
209 + }
210 +
211 + $user_id = $old_order->user_id;
212 + $user = get_userdata( $user_id );
213 + $user->membership_level = pmpro_getMembershipLevelForUser( $user_id );
214 +
215 + //generate billing failure email
216 + do_action( "pmpro_subscription_payment_failed", $old_order );
217 +
218 + $transaction = isset( $webhookNotification->transactions ) && is_array( $webhookNotification->transactions ) ?
219 + $webhookNotification->transactions[0] :
220 + null;
221 +
222 + //prep this order for the failure emails
223 + $morder = new \MemberOrder();
224 + $morder->user_id = $user_id;
225 + $morder->membership_id = $old_order->membership_id;
226 +
227 + $morder->billing = new stdClass();
228 + $morder->billing->name = isset( $transaction->billing_details->first_name ) && isset( $transaction->billing_details->last_name ) ?
229 + trim( $transaction->billing_details->first_name . " " . $transaction->billing_details->first_name ) :
230 + $old_order->billing->name;
231 +
232 + $morder->billing->street = isset( $transaction->billing_details->street_address ) ?
233 + $transaction->billing_details->street_address :
234 + $old_order->billing->street;
235 +
236 + $morder->billing->city = isset( $transaction->billing_details->locality ) ?
237 + $transaction->billing_details->locality :
238 + $old_order->billing->city;
239 +
240 + $morder->billing->state = isset( $transaction->billing_details->region ) ?
241 + $transaction->billing_details->region :
242 + $old_order->billing->state;
243 +
244 + $morder->billing->zip = isset( $transaction->billing_details->postal_code ) ?
245 + $transaction->billing_details->postal_code :
246 + $old_order->billing->zip;
247 +
248 + $morder->billing->country = isset( $transaction->billing_details->country_code_alpha2 ) ?
249 + $transaction->billing_details->country_code_alpha2 :
250 + $old_order->billing->country;
251 +
252 + $morder->billing->phone = $old_order->billing->phone;
253 +
254 + //Updates this order with the most recent orders payment method information and saves it.
255 + pmpro_update_order_with_recent_payment_method( $morder );
256 +
257 + // Email the user and ask them to update their credit card information
258 + $pmproemail = new \PMProEmail();
259 + $pmproemail->sendBillingFailureEmail( $user, $morder );
260 +
261 + // Email admin so they are aware of the failure
262 + $pmproemail = new \PMProEmail();
263 + $pmproemail->sendBillingFailureAdminEmail( get_bloginfo( "admin_email" ), $morder );
264 +
265 + $logstr[] = "Sent email to the member and site admin. Thanks.";
266 + pmpro_braintreeWebhookExit();
267 + }
268 +
269 + //subscription went past due
270 + if ( $webhookNotification->kind === Braintree_WebhookNotification::SUBSCRIPTION_WENT_PAST_DUE ) {
271 +
272 + $logstr[] = "The Braintree gateway informed us the subscription payment is past due";
273 +
274 + //need a subscription id
275 + if ( empty( $webhookNotification->subscription->id ) ) {
276 + $logstr[] = "No subscription ID.";
277 + pmpro_braintreeWebhookExit();
278 + }
279 +
280 + //figure out which order to attach to
281 + $old_order = new \MemberOrder();
282 + $old_order->getLastMemberOrderBySubscriptionTransactionID( $webhookNotification->subscription->id );
283 +
284 + if ( empty( $old_order ) ) {
285 + $logstr[] = "Couldn't find old order for failed payment with subscription id=" . $webhookNotification->subscription->id;
286 + pmpro_braintreeWebhookExit();
287 + }
288 +
289 + $user_id = $old_order->user_id;
290 + $user = get_userdata( $user_id );
291 + $user->membership_level = pmpro_getMembershipLevelForUser( $user_id );
292 +
293 + //generate billing failure email
294 + do_action( "pmpro_subscription_payment_failed", $old_order );
295 + do_action( "pmpro_subscription_payment_went_past_due", $old_order );
296 +
297 + $transaction = isset( $webhookNotification->transactions ) && is_array( $webhookNotification->transactions ) ?
298 + $webhookNotification->transactions[0] :
299 + null;
300 +
301 + //prep this order for the failure emails
302 + $morder = new \MemberOrder();
303 + $morder->user_id = $user_id;
304 + $morder->membership_id = $old_order->membership_id;
305 +
306 + $morder->billing->name = isset( $transaction->billing_details->first_name ) && isset( $transaction->billing_details->last_name ) ?
307 + trim( $transaction->billing_details->first_name . " " . $transaction->billing_details->first_name ) :
308 + $old_order->billing->name;
309 +
310 + $morder->billing->street = isset( $transaction->billing_details->street_address ) ?
311 + $transaction->billing_details->street_address :
312 + $old_order->billing->street;
313 +
314 + $morder->billing->city = isset( $transaction->billing_details->locality ) ?
315 + $transaction->billing_details->locality :
316 + $old_order->billing->city;
317 +
318 + $morder->billing->state = isset( $transaction->billing_details->region ) ?
319 + $transaction->billing_details->region :
320 + $old_order->billing->state;
321 +
322 + $morder->billing->zip = isset( $transaction->billing_details->postal_code ) ?
323 + $transaction->billing_details->postal_code :
324 + $old_order->billing->zip;
325 +
326 + $morder->billing->country = isset( $transaction->billing_details->country_code_alpha2 ) ?
327 + $transaction->billing_details->country_code_alpha2 :
328 + $old_order->billing->country;
329 +
330 + $morder->billing->phone = $old_order->billing->phone;
331 +
332 + //Updates this order with the most recent orders payment method information and saves it.
333 + pmpro_update_order_with_recent_payment_method( $morder );
334 +
335 + // Email the user and ask them to update their credit card information
336 + $pmproemail = new \PMProEmail();
337 + $pmproemail->sendBillingFailureEmail( $user, $morder );
338 +
339 + // Email admin so they are aware of the failure
340 + $pmproemail = new \PMProEmail();
341 + $pmproemail->sendBillingFailureAdminEmail( get_bloginfo( "admin_email" ), $morder );
342 +
343 + $logstr[] = "Sent email to the member and site admin. Thanks.";
344 + pmpro_braintreeWebhookExit();
345 + }
346 +
347 + //subscription expired
348 + if ( $webhookNotification->kind === Braintree_WebhookNotification::SUBSCRIPTION_EXPIRED ) {
349 + $logstr[] = "The Braintree gateway informed us the recurring payment plan has completed its required number of payments";
350 +
351 + //need a subscription id
352 + if ( empty( $webhookNotification->subscription->id ) ) {
353 + $logstr[] = "No subscription ID.";
354 + pmpro_braintreeWebhookExit();
355 + }
356 +
357 + //figure out which order to attach to
358 + $old_order = new \MemberOrder();
359 + $old_order->getLastMemberOrderBySubscriptionTransactionID( $webhookNotification->subscription->id );
360 +
361 + if ( empty( $old_order ) ) {
362 + $logstr[] = "Couldn't find old order for failed payment with subscription id=" . $webhookNotification->subscription->id;
363 + pmpro_braintreeWebhookExit();
364 + }
365 +
366 + $user_id = $old_order->user_id;
367 + $user = get_userdata( $user_id );
368 + $user->membership_level = pmpro_getMembershipLevelForUser( $user_id );
369 +
370 + //generate billing failure email
371 + do_action( "pmpro_subscription_expired", $old_order );
372 +
373 + $transaction = isset( $webhookNotification->transactions ) && is_array( $webhookNotification->transactions ) ?
374 + $webhookNotification->transactions[0] :
375 + null;
376 +
377 + // We don't currently allow billing limits (number_of_billing_cycles) on Braintree subscriptions.
378 + // But in case we get here, let's send the correct email to the admin.
379 + $myemail = new PMProEmail();
380 + $body = sprintf( __( "<p>A member's Braintree subscription has expired at your site. This typically happens if you've set up billing limits on your levels.</p><p>We have not removed the user's membership level.</p><p>You can view details on this user here: %s</p>", 'paid-memberships-pro' ), esc_url( admin_url( 'user-edit.php?user_id=' . $user_id ) ) );
381 + $myemail->template = 'braintree_subscription_expired';
382 + $myemail->subject = sprintf( __( "A member's Braintree subscription has expired at %s", 'paid-memberships-pro' ), get_bloginfo( 'name' ) );
383 + $myemail->data = array( 'body' => $body );
384 + $myemail->sendEmail( get_bloginfo( 'admin_email' ) );
385 +
386 + $logstr[] = "Sent email to the site admin. Thanks.";
387 + pmpro_braintreeWebhookExit();
388 + }
389 +
390 + //subscription cancelled (they used one l canceled)
391 + if ( $webhookNotification->kind === Braintree_WebhookNotification::SUBSCRIPTION_CANCELED ) {
392 + $logstr[] = 'The Braintree gateway cancelled the subscription plan';
393 +
394 + // Need a subscription id.
395 + if ( empty( $webhookNotification->subscription->id ) ) {
396 + $logstr[] = 'No subscription ID.';
397 + pmpro_braintreeWebhookExit();
398 + }
399 + $logstr[] = pmpro_handle_subscription_cancellation_at_gateway( $webhookNotification->subscription->id, 'braintree', get_option( 'pmpro_gateway_environment' ) );
400 + pmpro_braintreeWebhookExit();
401 + }
402 +
403 + pmpro_unhandled_webhook();
404 +
405 + /**
406 + * @since 1.9.5 - BUG FIX: Didn't terminate & save debug log for webhook event
407 + */
408 + pmpro_braintreeWebhookExit();
409 +
410 + /**
411 + * Fix address info for order/transaction
412 + *
413 + * @deprecated 3.2
414 + *
415 + * @param int $user_id
416 + * @param \MemberOrder $old_order
417 + *
418 + * @return \stdClass
419 + */
420 + function pmpro_braintreeAddressInfo( $user_id, $old_order ) {
421 + _deprecated_function( __FUNCTION__, '3.2' );
422 +
423 + // Grab billing info from the saved metadata as needed
424 +
425 + if ( ! isset( $old_order->billing ) ) {
426 + $old_order->billing = new \stdClass();
427 + }
428 +
429 + if ( empty ( $old_order->billing->name ) ) {
430 + $first_name = get_user_meta( $user_id, 'pmpro_bfirstname', true );
431 + $last_name = get_user_meta( $user_id, 'pmpro_blastname', true );
432 +
433 + if ( ! empty( $first_name ) && ! empty( $last_name ) ) {
434 + $old_order->billing->name = trim( "{$first_name} {$last_name}" );
435 + }
436 + }
437 +
438 + if ( empty( $old_order->billing->street ) ) {
439 + $address1 = get_user_meta( $user_id, 'pmpro_baddress', true );
440 + $address2 = get_user_meta( $user_id, 'pmpro_baddress2', true );
441 + $old_order->billing->street = ! empty( $address1 ) ? trim( $address1 ) : '';
442 + $old_order->billing->street .= ! empty( $address2 ) ? "\n" . trim( $address2 ) : '';
443 + }
444 +
445 + if ( empty( $old_order->billing->city ) ) {
446 + $city = get_user_meta( $user_id, 'pmpro_bcity', true );
447 + $old_order->billing->city = ! empty( $city ) ? trim( $city ) : '';
448 + }
449 +
450 + if ( empty( $old_order->billing->state ) ) {
451 + $state = get_user_meta( $user_id, 'pmpro_bstate', true );
452 + $old_order->billing->state = ! empty( $state ) ? trim( $state ) : '';
453 + }
454 +
455 + if ( empty( $old_order->billing->zip ) ) {
456 + $zip = get_user_meta( $user_id, 'pmpro_bzipcode', true );
457 + $old_order->billing->zip = ! empty( $zip ) ? trim( $zip ) : '';
458 + }
459 +
460 + if ( empty( $old_order->billing->country ) ) {
461 + $country = get_user_meta( $user_id, 'pmpro_bcountry', true );
462 + $old_order->billing->country = ! empty( $country ) ? trim( $country ) : '';
463 + }
464 +
465 + $old_order->updateBilling();
466 +
467 + return $old_order->billing;
468 + }
469 +
470 + /**
471 + * Exit the Webhook handler, and save the debug log (if needed)
472 + */
473 + function pmpro_braintreeWebhookExit() {
474 +
475 + global $logstr;
476 +
477 + //Log the info (if there is any)
478 + if ( ! empty( $logstr ) ) {
479 +
480 + $logstr[] = "\n-------------\n";
481 +
482 + $debuglog = implode( "\n", $logstr );
483 +
484 + //log in file or email?
485 + if ( defined( 'PMPRO_BRAINTREE_WEBHOOK_DEBUG' ) && PMPRO_BRAINTREE_WEBHOOK_DEBUG === "log" ) {
486 + //file
487 + $logfile = apply_filters( 'pmpro_braintree_webhook_logfile', pmpro_get_restricted_file_path( 'logs', 'braintree-webhook.txt' ) );
488 + $loghandle = fopen( $logfile, "a+" );
489 + fwrite( $loghandle, $debuglog );
490 + fclose( $loghandle );
491 + } else if ( defined( 'PMPRO_BRAINTREE_WEBHOOK_DEBUG' ) ) {
492 + /**
493 + * @since 1.9.5 - BUG FIX: We specifically care about errors, not strings at position 0
494 + */
495 + //email
496 + if ( false !== strpos( PMPRO_BRAINTREE_WEBHOOK_DEBUG, "@" ) ) {
497 + $log_email = PMPRO_BRAINTREE_WEBHOOK_DEBUG; //constant defines a specific email address
498 + } else {
499 + $log_email = get_option( "admin_email" );
500 + }
501 +
502 + wp_mail( $log_email, get_option( "blogname" ) . " Braintree Webhook Log", nl2br( esc_html( $debuglog ) ) );
503 + }
504 + }
505 +
506 + exit;
507 + }
508 +