Source: gateways/my-calendar-stripe.php

<?php
/**
 * My Calendar Pro - Stripe
 *
 * @category Gateways
 * @package  My Calendar Pro
 * @author   Joe Dolson
 * @license  GPLv2 or later
 * @link     https://www.joedolson.com/my-calendar-pro/
 */

add_action( 'mcs_process_payment', 'mcs_stripe_ipn' );
/**
 * Process events sent from from Stripe
 *
 * @param string $gateway Gateway identifier.
 *
 * Handles the charge.refunded event & source.chargeable events.
 * Uses do_action( 'mcs_stripe_event', $charge ) if you want custom handling.
 */
function mcs_stripe_ipn( $gateway ) {
	if ( isset( $_REQUEST['mcsipn'] ) && 'true' === $_REQUEST['mcsipn'] ) {
		global $mcs_version;

		// these all need to be set from Stripe data.
		$stripe_options = get_option( 'mcs_stripe' );
		if ( 'true' === get_option( 'mcs_use_sandbox' ) ) {
			$secret_key = trim( $stripe_options['test_secret'] );
		} else {
			$secret_key = trim( $stripe_options['prod_secret'] );
		}
		if ( ! $secret_key ) {
			return;
		}
		\Stripe\Stripe::setAppInfo(
			'WordPress MyCalendarStripe',
			$mcs_version,
			'https://www.joedolson.com/contact/'
		);
		\Stripe\Stripe::setApiKey( $secret_key );
		\Stripe\Stripe::setApiVersion( '2019-08-14' );

		// retrieve the request's body and parse it as JSON.
		$body = @file_get_contents( 'php://input' );

		// grab the event information.
		$event_json = json_decode( $body );
		if ( ! is_object( $event_json ) ) {
			status_header( 418 );
			die;
		}
		// this will be used to retrieve the event from Stripe.
		$event_id = $event_json->id;

		if ( isset( $event_json->id ) ) {

			try {
				// to verify this is a real event, we re-retrieve the event from Stripe.
				$event  = \Stripe\Event::retrieve( $event_id );
				$object = $event->data->object->object;

				switch ( $object ) {
					case 'charge':
						$object      = $event->data->object;
						$item_number = $object->metadata->item_number;
						break;
					case 'payment_intent':
						$object      = $event->data->object;
						$item_number = $object->metadata->item_number;
						break;
					default:
						// Need to return 200 on all other situations.
						status_header( 200 );
						die;
				}
				if ( ! $item_number ) {
					// Not mine to handle; fail silently. This message should be handled by something else.
					die;
				}
				$email  = mcs_get_payment_field( $item_number, 'payer_email' );
				$status = mcs_get_payment_field( $item_number, 'status' );
				$first  = mcs_get_payment_field( $item_number, 'first_name' );
				$last   = mcs_get_payment_field( $item_number, 'last_name' );
				do_action( 'mcs_stripe_event', $object );

			} catch ( Exception $e ) {
				if ( function_exists( 'mcs_log_error' ) ) {
					mcs_log_error( $e );
				}
				// Return an HTTP 202 (Accepted) to stop repeating this event.
				// An error is thrown if an event is sent to the site for a transaction in test mode after the site is switched to live mode or vice versa.
				status_header( 202 );
				die;
			}
			switch ( $event->type ) {
				case 'charge.refunded':
					$partial = ( $object->refunded ) ? false : true;
					if ( $partial ) {
						$applied = get_transient( 'mcs_stripe_' . $item_number );
						if ( 'refund_applied' === $applied ) {
							status_header( 202 );
							die;
						}
						set_transient( 'mcs_stripe_' . $item_number, 'refund_applied', 10 );
						$amount  = ( mcs_zerodecimal_currency() ) ? $object->amount_refunded : $object->amount_refunded / 100;
						$details = array(
							'id'     => $item_number,
							'name'   => $first . ' ' . $last,
							'email'  => $email,
							'amount' => wp_strip_all_tags( mcs_money_format( $amount ) ),
						);
						/**
						 * Filter text of email sent after a partial refund through Stripe.
						 *
						 * @hook mcs_stripe_partial_refund_email
						 *
						 * @param {string} $text Text of email with template tags. Available {amount}, {name}, {email}, {id}.
						 *
						 * @return {string}
						 */
						$template = apply_filters( 'mcs_stripe_partial_refund_email', __( 'A partial refund on your purchase has been administered. The refund should appear on your credit card statement within 5-10 days. Refunded amount: {amount}.', 'my-calendar-pro' ) );
						$body     = mcs_draw_template( $details, $template );
						$sitename = get_bloginfo( 'name' );
						// Translators: sitename sending refund.
						wp_mail( $details['email'], sprintf( __( 'Partial Refund from %s', 'my-calendar-pro' ), $sitename ), $body );
						status_header( 200 );
					} else {
						if ( ! ( 'Refunded' === $status ) ) {
							update_post_meta( $item_number, '_is_paid', 'Refunded' );
							$details = array(
								'id'    => $item_number,
								'name'  => $first . ' ' . $last,
								'email' => $email,
							);
							mcs_send_notifications( 'Refunded', $details );
							status_header( 200 );
						} else {
							status_header( 202 );
						}
					}
					die;
					break;
				// Successful payment.
				case 'payment_intent.succeeded':
					if ( ! ( 'Completed' === $status ) ) {
						$paid           = $object->amount_received;
						$transaction_id = $object->id;
						$payment_status = 'Completed';

						$price  = ( mcs_zerodecimal_currency() ) ? $paid : $paid / 100;
						$values = array(
							'item_number' => $item_number,
							'txn_id'      => $transaction_id,
							'price'       => $price,
							'mc_fee'      => '',
							'payer_first' => $first,
							'payer_last'  => $last,
							'payer_email' => $email,
							'status'      => 'Completed',
						);
						mcs_process_payment( $values, 'Completed' );
					}
					status_header( 200 );
					die;
					break;
				default:
					status_header( 200 );
					die;
			}
		} else {
			status_header( 400 );
			die;
		}
	}

	return;
}

/**
 * Set up form for making a Stripe payment.
 *
 * @param float   $total Total amount of payment.
 * @param float   $discount_rate Percentage discount.
 * @param array   $discounts Discount info.
 * @param bool    $discount Is discount active.
 * @param integer $item_number ID for this payment. [Not currently available; may need rewriting.].
 *
 * @return string.
 */
function mcs_stripe_insert_form( $total, $discount_rate, $discounts, $discount, $item_number = null ) {
	// The form only displays after a POST request, and these fields are required.
	$name  = isset( $_POST['mcs_name'] ) ? sanitize_text_field( wp_unslash( $_POST['mcs_name'] ) ) : '';
	$email = isset( $_POST['mcs_email'] ) ? sanitize_email( wp_unslash( $_POST['mcs_email'] ) ) : '';

	$form = '<form id="mcs-payment-form" action="/charge" method="post">
	<div class="mcs-stripe-hidden-fields">
		<input type="hidden" name="item_number" id="mcs-payment-id" value="' . esc_attr( $item_number ) . '" />
		<input type="hidden" name="_mcs_secret" id="mcs_client_secret" value="" />
	</div>
	<div class="stripe">';
	// Hidden form fields.
	$form .= "<div id='mcs-card'>
			<p class='form-row'>
			  <label for='mcs_name'>" . __( 'Name', 'my-calendar-pro' ) . "</label><input id='mcs_name' name='name' value='" . esc_attr( $name ) . "' required>
			</p>
			<p class='form-row'>
			  <label for='mcs_email'>" . __( 'Email Address', 'my-calendar-pro' ) . "</label><input id='mcs_email' name='email' type='email' value='" . esc_attr( $email ) . "'  required>
			</p>
			<div class='form-row'>
				<label for='mcs-card-element'>" . __( 'Credit or debit card', 'my-calendar-pro' ) . "</label>
				<div id='mcs-card-element'></div>
			</div>
		</div>";

	$form .= "<div id='mcs-card-errors' class='mcs-stripe-errors' role='alert'></div>";
	$form .= "<input type='submit' name='stripe_submit' id='mcs-stripe-submit' class='button button-primary' value='" . esc_attr( apply_filters( 'mcs_gateway_button_text', __( 'Pay Now', 'my-calendar-pro' ), 'stripe' ) ) . "' />";
	$form .= '</div></form>';

	return $form;
}

/**
 * Generate payment intent for Stripe.
 *
 * @param int $total Total of payment in cents.
 * @param int $item_number Payment ID.
 *
 * @return object
 */
function mcs_generate_intent( $total, $item_number ) {
	$stripe_options = get_option( 'mcs_stripe' );
	$payment        = mcs_get_payment( $item_number );

	// check if we are using test mode.
	if ( 'true' === get_option( 'mcs_use_sandbox' ) ) {
		$secret_key = trim( $stripe_options['test_secret'] );
	} else {
		$secret_key = trim( $stripe_options['prod_secret'] );
	}
	if ( ! $secret_key && current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'Your Stripe API keys have not been set. Generate your API keys at Stripe.com', 'my-calendar-pro' ) );
	}
	// If blog name not provided, use URL.
	$remove      = array( '<', '>', '"', '\'' );
	$host        = wp_parse_url( home_url() );
	$blogname    = ( '' === trim( get_bloginfo( 'name' ) ) ) ? $host['host'] : get_bloginfo( 'name' );
	$description = $blogname;

	$intent_id = ! empty( $payment['metadata'] ) ? unserialize( $payment['metadata'] )['intent_id'] : false;
	\Stripe\Stripe::setApiKey( $secret_key );
	\Stripe\Stripe::setApiVersion( '2019-08-14' );

	if ( ! $intent_id ) {
		// Translators: blog name, comma-separated list of events represented in this purchase.
		$description = sprintf( __( 'Event Submissions at %s', 'my-calendar-pro' ), get_bloginfo( 'name' ) );
		if ( 500 >= strlen( $description ) ) {
			$description = substr( $description, 0, 497 ) . '...';
		}
		$intent = \Stripe\PaymentIntent::create(
			array(
				'amount'               => $total,
				'currency'             => get_option( 'mcs_currency' ),
				'payment_method_types' => array( 'card' ),
				'statement_descriptor' => strtoupper( substr( sanitize_text_field( str_replace( $remove, '', $blogname ) ), 0, 22 ) ),
				'metadata'             => array( 'item_number' => $item_number ),
				'description'          => $description,
			)
		);
		mcs_update_payment_field( $item_number, 'metadata', serialize( array( 'intent_id' => $intent->id ) ) );
	} else {
		$intent = \Stripe\PaymentIntent::retrieve( $intent_id );
		$amount = $intent->amount;
		$desc   = $intent->description;
		// $amount is int; $total is float.
		if ( ! ( $amount === (int) $total && $desc === $description ) ) {
			$intent = \Stripe\PaymentIntent::update(
				$intent_id,
				array(
					'amount'      => $total,
					'description' => $description,
				)
			);
		}
	}

	return $intent;
}

/**
 * Provide response message from PayPal.
 *
 * @param array  $response Data from PayPal.
 * @param string $mcs Query string value.
 * @param array  $payment Payment data.
 * @param string $gateway Gateway name.
 *
 * @return string
 */
function mcs_stripe_response( $response, $mcs, $payment, $gateway ) {
	$ret = '';
	if ( 'stripe' === $gateway ) {
		$hash    = $payment['hash'];
		$receipt = add_query_arg( 'mcs_receipt', $hash, home_url() );
		switch ( $mcs ) {
			case 'thanks':
				// Translators: Receipt link.
				$ret = "<p class='notice'>" . sprintf( __( 'Thank you for your purchase! You will receive an email with your payment key when your payment is completed. %s', 'my-calendar-pro' ), '<a href="' . esc_url( $receipt ) . '">Receipt</a>' ) . '</p>';
				break;
			case 'cancel':
				$ret = __( 'Sorry that you decided to cancel your purchase! Contact us if you have any questions!', 'my-calendar-pro' );
				break;
		}
	}

	return $ret;
}
add_filter( 'mcs_gateway_response', 'mcs_stripe_response', 10, 4 );

/**
 * Insert Stripe form
 *
 * @param string $form Form HTML.
 * @param string $gateway Name of gateway.
 * @param array  $args Gateway arguments.
 *
 * @return string
 */
function mcs_stripe_form( $form, $gateway, $args ) {
	if ( 'stripe' === $gateway ) {
		$price = $args['price'] * 100; // Adjust to remove decimals.
		$form .= mcs_stripe_insert_form( $price, $args['discount_rate'], $args['discounts'], $args['discount'] );
	}

	return $form;
}
add_filter( 'mcs_payment_gateway', 'mcs_stripe_form', 10, 3 );

/**
 * Import Stripe class.
 *
 * @package Stripe
 */
if ( ! class_exists( '\Stripe\Stripe' ) ) {
	require_once __DIR__ . '/vendor/stripe-php/init.php';
}

/**
 * Return all currencies supported by Stripe.
 *
 * @return array of currency ids
 */
function mcs_stripe_supported() {
	return array( 'USD', 'EUR', 'AUD', 'CAD', 'GBP', 'JPY', 'NZD', 'CHF', 'HKD', 'SGD', 'SEK', 'DKK', 'PLN', 'NOK', 'HUF', 'ILS', 'MXN', 'BRL', 'MYR', 'PHP', 'TWD', 'THB' );
}
add_filter( 'mcs_currencies', 'mcs_stripe_currencies', 10, 1 );

/**
 * If this gateway is active, limit currencies to supported currencies.
 *
 * @param array $currencies Currencies supported.
 *
 * @return return full currency array.
 */
function mcs_stripe_currencies( $currencies ) {
	$mcs_gateway = get_option( 'mcs_gateway', 'paypal' );

	if ( 'stripe' === $mcs_gateway ) {
		$stripe = mcs_stripe_supported();
		$return = array();
		foreach ( $stripe as $currency ) {
			$keys = array_keys( $currencies );
			if ( in_array( $currency, $keys, true ) ) {
				$return[ $currency ] = $currencies[ $currency ];
			}
		}

		return $return;
	}

	return $currencies;
}

/**
 * Get Stripe webhook status.
 *
 * @return string
 */
function mcs_stripe_setup_status() {
	$note           = '';
	$stripe_options = get_option( 'mcs_stripe', array() );
	$test_mode      = get_option( 'mcs_use_sandbox' );
	if ( ! empty( $stripe_options ) ) {
		$test_secret_key = trim( $stripe_options['test_secret'] );
		$test_webhook_id = get_option( 'mcs_stripe_test_webhook', false );
		$live_secret_key = trim( $stripe_options['prod_secret'] );
		$live_webhook_id = get_option( 'mcs_stripe_live_webhook', false );

		$setup = ( $test_secret_key && $live_secret_key ) ? true : false;
	} else {
		$test_secret_key = '';
		$test_webhook_id = '';
		$live_secret_key = '';
		$live_webhook_id = '';

		$setup = false;
	}

	if ( $setup && ( $test_webhook_id || $live_webhook_id ) ) {
		if ( $test_webhook_id && $test_secret_key ) {
			\Stripe\Stripe::setApiKey( $test_secret_key );
			\Stripe\Stripe::setApiVersion( '2019-08-14' );

			try {
				$test_endpoint = \Stripe\WebhookEndpoint::retrieve( $test_webhook_id );
			} catch ( Exception $e ) {
				if ( function_exists( 'mcs_log_error' ) ) {
					mcs_log_error( $e );
				}
				$test_endpoint = (object) array( 'status' => 'missing' );
			}
		} else {
			$test_endpoint = (object) array( 'status' => 'not created' );
		}
		if ( $live_webhook_id && $live_secret_key ) {
			\Stripe\Stripe::setApiKey( $live_secret_key );
			\Stripe\Stripe::setApiVersion( '2019-08-14' );

			try {
				$live_endpoint = \Stripe\WebhookEndpoint::retrieve( $live_webhook_id );
			} catch ( Exception $e ) {
				if ( function_exists( 'mcs_log_error' ) ) {
					mcs_log_error( $e );
				}
				$live_endpoint = (object) array( 'status' => 'missing' );
			}
		} else {
			$live_endpoint = (object) array(
				'status' => 'not created',
				'url'    => add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ),
			);
		}
		// Translators: Live webhook URL, live webhook status, test webhook URL.
		$note = sprintf( __( 'Your webhooks point to <code>%1$s</code>. Your live webhook is currently <strong>%2$s</strong>; your test webhook is <strong>%3$s</strong>.', 'my-calendar-pro' ), $live_endpoint->url, $live_endpoint->status, $test_endpoint->status );

		$updates  = ( isset( $_POST['mcs_gateways'] ) ) ? $_POST['mcs_gateways'] : false;
		$runsetup = false;
		if ( $updates && isset( $updates['stripe']['update_webhooks'] ) ) {
			$runsetup = true;
		}

		$note .= ( true === $runsetup ) ? ' <strong class="updated">' . __( 'Your Stripe webhook endpoints have been automatically created or updated.', 'my-calendar-pro' ) . '</strong>' : '';

	} elseif ( $setup ) {
		if ( $live_secret_key ) {
			\Stripe\Stripe::setApiKey( $live_secret_key );
			\Stripe\Stripe::setApiVersion( '2019-08-14' );

			$endpoints = \Stripe\WebhookEndpoint::all( array( 'limit' => 16 ) );
			$count     = 0;
			foreach ( $endpoints as $endpoint ) {
				if ( add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) === $endpoint->url ) {
					// Translators: Webhook URL.
					$note = sprintf( __( 'You have an existing live Stripe webhook at <code>%s</code>.', 'my-calendar-pro' ), $endpoint->url );
					++$count;
				}
			}
			if ( $count > 1 ) {
				// Translators: webhook URL.
				$note .= sprintf( __( 'You currently have multiple live Stripe webhooks pointing to <code>%s</code>. Log-in to your Stripe Dashboard to delete duplicate webhooks. Multiple webhooks can lead to confusing notifications to customers.', 'my-calendar-pro' ), add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) );
			}
		}
		if ( $test_secret_key ) {
			\Stripe\Stripe::setApiKey( $test_secret_key );
			\Stripe\Stripe::setApiVersion( '2019-08-14' );

			$endpoints = \Stripe\WebhookEndpoint::all( array( 'limit' => 16 ) );
			$count     = 0;
			foreach ( $endpoints as $endpoint ) {
				if ( add_query_arg( 'mcsipn', 'true', esc_url( home_url() ) ) === $endpoint->url ) {
					// Translators: Webhook URL.
					$note .= ' ' . sprintf( __( 'You have an existing test Stripe webhook at <code>%s</code>.', 'my-calendar-pro' ), $endpoint->url );
					++$count;
				}
			}
			if ( $count > 1 ) {
				// Translators: Webhook URL.
				$note .= sprintf( __( 'You currently have multiple test Stripe webhooks pointing to <code>%s</code>. Log-in to your Stripe Dashboard to delete duplicate webhooks. Multiple webhooks can lead to confusing notifications to customers.', 'my-calendar-pro' ), add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) );
			}
		}
		if ( '' === $note ) {
			// Translators: Webhook URL.
			$note = sprintf( __( 'You need to add <code>%s</code> as a Webhook URL in your Stripe account at Stripe > Dashboard > Settings > Webhooks. My Tickets: Stripe will attempt to configure your webhook automatically when you save your Stripe API keys.', 'my-calendar-pro' ), add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) );
		}
	} else {
		// Translators: Webhook URL.
		$note = sprintf( __( 'You need to add <code>%s</code> as a Webhook URL in your Stripe account at Stripe > Dashboard > Settings > Webhooks. My Tickets: Stripe will attempt to configure your webhook automatically when you save your Stripe API keys.', 'my-calendar-pro' ), add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) );
	}

	if ( $live_secret_key && 'true' !== $test_mode ) {
		\Stripe\Stripe::setApiKey( $live_secret_key );
		\Stripe\Stripe::setApiVersion( '2019-08-14' );

		$endpoints = \Stripe\WebhookEndpoint::all( array( 'limit' => 16 ) );
		$count     = 0;
		foreach ( $endpoints as $endpoint ) {
			if ( add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) === $endpoint->url ) {
				++$count;
			}
		}
		if ( $count > 1 ) {
			// Translators: Webhook URL.
			$note .= sprintf( __( 'You currently have multiple live Stripe webhooks pointing to <code>%s</code>. Log-in to your Stripe Dashboard to delete duplicate webhooks. Multiple webhooks can lead to confusing notifications to customers.', 'my-calendar-pro' ), add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) );
		}
	}
	if ( $test_secret_key && 'true' === $test_mode ) {
		\Stripe\Stripe::setApiKey( $test_secret_key );
		\Stripe\Stripe::setApiVersion( '2019-08-14' );

		$endpoints = \Stripe\WebhookEndpoint::all( array( 'limit' => 16 ) );
		$count     = 0;
		foreach ( $endpoints as $endpoint ) {
			if ( add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) === $endpoint->url ) {
				++$count;
			}
		}
		if ( $count > 1 ) {
			// Translators: Webhook URL.
			$note .= sprintf( __( 'You currently have multiple test Stripe webhooks pointing to <code>%s</code>. Log-in to your Stripe Dashboard to delete duplicate webhooks. Multiple webhooks can lead to confusing notifications to customers.', 'my-calendar-pro' ), add_query_arg( 'mcsipn', 'true', esc_url( get_permalink( get_option( 'mcs_submit_id' ) ) ) ) );
		}
	}

	return $note;
}
add_filter( 'mcs_response_messages', 'mcs_stripe_messages', 10, 2 );