Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
<?php
/**
 * Webhooks
 *
 * @package SimplePay\Pro\Webhooks
 * @copyright Copyright (c) 2020, Sandhills Development, LLC
 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since 3.5.0
 */

namespace SimplePay\Pro\Webhooks;

use SimplePay\Core\Payments\Stripe_API;
use SimplePay\Pro\Webhooks\Database;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Attempt to find a Webhook based on an incoming Event ID.
 *
 * @since 3.5.0
 *
 * @param string $event_id Event ID.
 * @return bool False if no webhook is found.
 */
function get_recorded_event( $event_id ) {
	$db = new Database\Query();

	$webhook = $db->query(
		array(
			'event_id' => $event_id,
			'number'   => 1,
		)
	);

	return ! empty( $webhook );
}

/**
 * Record a webhook's event in the database for future tracking.
 *
 * @since 3.5.0
 *
 * @param \Stripe\Event $event Stripe Event.
 * @return bool False if no webhook is recorded.
 */
function record_event( $event ) {
	if ( empty( $event->id ) ) {
		return false;
	}

	$query = new Database\Query();

	return $query->add_item(
		array(
			'event_id'     => $event->id,
			'event_type'   => $event->type,
			'livemode'     => $event->livemode,
			'date_created' => date( 'Y-m-d H:i:s', $event->created ),
		)
	);
}

/**
 * Returns the latest Webhook Event.
 *
 * @since 4.0.1
 *
 * @param bool $livemode Stripe payment mode.
 * @return false|object {
 *   False if no event is found.
 *
 *   @type string $event_id Stripe Event ID.
 *   @type string $event_type Stripe Event type.
 *   @type int    $livemode Stripe Event livemode.
 *   @type string $date_created Stripe Event creation date.
 * }
 */
function get_latest_event( $livemode ) {
	$db = new \SimplePay\Pro\Webhooks\Database\Query();

	$webhooks = $db->query(
		array(
			'number'   => 1,
			'livemode' => $livemode,
		)
	);

	if ( empty( $webhooks ) ) {
		return false;
	}

	return current( $webhooks );
}

/**
 * Determines if a Webhook Event has been logged within the last 48 hours.
 *
 * @since 4.0.1
 *
 * @return boolean
 */
function has_recent_event() {
	$livemode  = ! simpay_is_test_mode();
	$event     = get_latest_event( $livemode );

	// If no webhooks are recorded at all, assume setup is still occuring.
	if ( false === $event ) {
		return true;
	}

	$timeframe = DAY_IN_SECONDS * 2;

	return ( time() - strtotime( $event->date_created ) ) < $timeframe;
}

/**
 * Handle a webhook.
 *
 * @since 3.5.0
 *
 * @see SimplePay\Pro\Webhooks\get_event_whitelist()
 *
 * @param object $event Stripe event.
 * @throws SimplePay\Pro\Webhooks\Exception\Invalid_Event_Type If the event type is not registered.
 * @throws SimplePay\Pro\Webhooks\Exception\Invalid_Event_Handler If the event type has no callable handler class.
 * @throws SimplePay\Pro\Webhooks\Exception\Duplicate_Attempt If the webhook has already been processed.
 */
function process_event( $event ) {
	$webhooks = get_event_whitelist();

	// Event isn't whitelisted.
	if ( ! isset( $webhooks[ $event->type ] ) ) {
		throw new Exception\Invalid_Event_Type( esc_html__( 'Event type not registered. No processing was done.', 'simple-pay' ) );
	}

	// Event can't be handled.
	if ( ! class_exists( $webhooks[ $event->type ] ) ) {
		throw new Exception\Invalid_Event_Handler( esc_html__( 'Event handler not found. No processing was done.', 'simple-pay' ) );
	}

	$record = get_recorded_event( $event->id );

	// Webhook has already been recorded.
	if ( false !== $record ) {
		throw new Exception\Duplicate_Attempt( esc_html__( 'Webhook has been previously received. No further processing was done.', 'simple-pay' ) );
	}

	$handler = new $webhooks[ $event->type ]( $event );
	$handler->handle();

	// Record a successful event.
	record_event( $event );
}
// Run a little late so custom code can run before processing by default.
add_action( 'simpay_webhook_event', __NAMESPACE__ . '\\process_event', 20 );

/**
 * Legacy Webhook URL handling.
 *
 * @link https://stripe.com/docs/recipes/installment-plan
 * @link https://stripe.com/docs/webhooks
 *
 * @since unkown
 */
function process_legacy_listener() {
	if ( ! ( isset( $_GET['simple-pay-listener'] ) && $_GET['simple-pay-listener'] == 'stripe' ) ) {
		return;
	}

	try {
		$event = verify_webhook( @file_get_contents( 'php://input' ) );

		/* This action is documented in includes/pro/rest-api/v1/class-webhooks-controller.php. */
		do_action( 'simpay_webhook_event', $event );

		status_header( 200 );
	} catch ( Exception\Invalid_Event_Type $e ) {
		// We can't find this event type, tell Stripe everything is good.
		status_header( 200 );

	} catch ( Exception\Invalid_Event_Handler $e ) {
		// We can't find anything to do with this event, tell Stripe everything is good.
		status_header( 200 );

	} catch ( Exception\Duplicate_Attempt $e ) {
		// Processing for this webhook has already happened, tell Stripe everything is good.
		status_header( 200 );

	} catch ( \Exception $e ) {
		// Something went wrong running the event type callback, tell Stripe to try again.
		status_header( 400 );
	}

	exit();
}
// Run after the REST API handler.
add_action( 'init', __NAMESPACE__ . '\\process_legacy_listener', 30 );

/**
 * Get a list of webhook events we want to handle.
 *
 * @since 3.5.0
 *
 * @return array
 */
function get_event_whitelist() {
	// Event Type => Handler Class.
	$webhooks = array(
		'invoice.payment_succeeded'  => '\\SimplePay\\Pro\\Webhooks\\Webhook_Invoice_Payment_Succeeded',
		'invoice.upcoming'           => '\\SimplePay\\Pro\\Webhooks\\Webhook_Invoice_Upcoming',
		'payment_intent.succeeded'   => '\\SimplePay\\Pro\\Webhooks\\Webhook_Payment_Intent_Succeeded',
		'checkout.session.completed' => '\\SimplePay\\Pro\\Webhooks\\Webhook_Checkout_Session_Completed',
		'plan.updated'               => '\\SimplePay\\Pro\\Webhooks\\Webhook_Plan_Updated',
		'charge.failed'              => '\\SimplePay\\Pro\\Webhooks\\Webhook_Charge_Failed',
	);

	/**
	 * Filter the webhooks to handle.
	 *
	 * @since 3.5.0
	 *
	 * @param array $webhooks Webhooks to handle.
	 */
	return apply_filters( 'simpay_webhooks_get_event_whitelist', $webhooks );
}

/**
 * Retrieve the endpoint secret for the current mode.
 *
 * @since 3.5.0
 * @since 3.9.0 Added required $livemode paramter.
 *
 * @param bool $livemode If using livemode.
 * @return string
 */
function get_endpoint_secret( $livemode ) {
	$prefix = false === $livemode
		? 'test'
		: 'live';

	return simpay_get_setting( $prefix . '_webhook_endpoint_secret', '' );
}

/**
 * Determine if a webhook can be verified.
 *
 * @since 3.5.0
 *
 * @param bool $livemode If using livemode.
 * @return bool
 */
function can_verify_webhook_endpoint( $livemode ) {
	return ! empty( get_endpoint_secret( $livemode ) );
}

/**
 * Verify a Webhook.
 *
 * If a Webhook Endpoint secret exists verify the signature.
 * If no secret exists retrieve the data again from Stripe and use that object.
 *
 * @link https://stripe.com/docs/webhooks/signatures
 *
 * @since 3.5.0
 *
 * @param object $payload Event payload.
 * @throws \Stripe\Exception\ApiErrorException If the something goes wrong with Stripe verifying the Webhook with a secret.
 * @throws \Exception If the webhook cannot be verified by retrieving it with Stripe.
 * @return \Stripe\Event $event Stripe Event.
 */
function verify_webhook( $payload ) {
	$event          = false;
	$payload_object = json_decode( $payload );

	// @todo Update simpay_get_secret_key() to accept livemode.
	$prefix = false === $payload_object->livemode
		? 'test'
		: 'live';

	$api_key = simpay_get_setting( $prefix . '_secret_key', '' );

	// We do not have the necessary information for endpoint verification.
	// Instead try getting the event from the Stripe API.
	if ( ! can_verify_webhook_endpoint( $payload_object->livemode ) ) {
		if ( ! $payload_object ) {
			return $event;
		}

		$event = Stripe_API::request(
			'Event',
			'retrieve',
			$payload_object->id,
			array(
				'api_key' => $api_key,
			)
		);
	} else {
		$endpoint_secret = get_endpoint_secret( $payload_object->livemode );
		$sig_header      = isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] )
			? $_SERVER['HTTP_STRIPE_SIGNATURE']
			: false;

		if ( ! $sig_header ) {
			return $event;
		}

		$event = \Stripe\Webhook::constructEvent(
			$payload,
			$sig_header,
			$endpoint_secret
		);
	}

	return $event;
}