<?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 );