279 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * Webhook Functions
 | |
|  *
 | |
|  * @package    easy-digital-downloads
 | |
|  * @subpackage Gateways\PayPal\Webhooks
 | |
|  * @copyright  Copyright (c) 2021, Sandhills Development, LLC
 | |
|  * @license    GPL2+
 | |
|  * @since      2.11
 | |
|  */
 | |
| 
 | |
| namespace EDD\Gateways\PayPal\Webhooks;
 | |
| 
 | |
| use EDD\Gateways\PayPal\API;
 | |
| use EDD\Gateways\PayPal\Exceptions\API_Exception;
 | |
| use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
 | |
| 
 | |
| /**
 | |
|  * Returns the webhook URL.
 | |
|  *
 | |
|  * @since 2.11
 | |
|  * @return string
 | |
|  */
 | |
| function get_webhook_url() {
 | |
| 	return rest_url( Webhook_Handler::REST_NAMESPACE . '/' . Webhook_Handler::REST_ROUTE );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Returns the ID of the webhook.
 | |
|  *
 | |
|  * @param string $mode API mode. Either `sandbox` or `live`. If omitted, current store mode is used.
 | |
|  *
 | |
|  * @since 2.11
 | |
|  * @return string|false
 | |
|  */
 | |
| function get_webhook_id( $mode = '' ) {
 | |
| 	if ( empty( $mode ) ) {
 | |
| 		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
 | |
| 	}
 | |
| 
 | |
| 	return get_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ) );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Returns the list of webhook events that EDD requires.
 | |
|  *
 | |
|  * @link  https://developer.paypal.com/docs/api-basics/notifications/webhooks/event-names/#sales
 | |
|  *
 | |
|  * @todo  Would be nice to use the EDD 3.0 registry for this at some point.
 | |
|  *
 | |
|  * @param string $mode Store mode. Either `sandbox` or `live`.
 | |
|  *
 | |
|  * @since 2.11
 | |
|  * @return array
 | |
|  */
 | |
| function get_webhook_events( $mode = '' ) {
 | |
| 	$events = array(
 | |
| 		'PAYMENT.CAPTURE.COMPLETED' => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Completed',
 | |
| 		'PAYMENT.CAPTURE.DENIED'    => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Denied',
 | |
| 		'PAYMENT.CAPTURE.REFUNDED'  => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Refunded',
 | |
| 		'PAYMENT.CAPTURE.REVERSED'  => '\\EDD\\Gateways\\PayPal\\Webhooks\\Events\\Payment_Capture_Refunded',
 | |
| 	);
 | |
| 
 | |
| 	/**
 | |
| 	 * Filters the webhook events.
 | |
| 	 *
 | |
| 	 * @param array  $events Array of events that PayPal will send webhooks for.
 | |
| 	 * @param string $mode   Mode the webhook is being created in. Either `sandbox` or `live`.
 | |
| 	 *
 | |
| 	 * @since 2.11
 | |
| 	 */
 | |
| 	return (array) apply_filters( 'edd_paypal_webhook_events', $events, $mode );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Creates a webhook.
 | |
|  *
 | |
|  * @param string $mode Store mode. Either `sandbox` or `live`. If omitted, current store mode is used.
 | |
|  *
 | |
|  * @return true
 | |
|  * @throws API_Exception
 | |
|  * @throws Authentication_Exception
 | |
|  */
 | |
| function create_webhook( $mode = '' ) {
 | |
| 	if ( ! is_ssl() ) {
 | |
| 		throw new API_Exception( __( 'An SSL certificate is required to create a PayPal webhook.', 'easy-digital-downloads' ) );
 | |
| 	}
 | |
| 
 | |
| 	if ( empty( $mode ) ) {
 | |
| 		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
 | |
| 	}
 | |
| 
 | |
| 	$webhook_url = get_webhook_url();
 | |
| 
 | |
| 	$api = new API( $mode );
 | |
| 
 | |
| 	// First, list webhooks in case it's already added.
 | |
| 	try {
 | |
| 		$response = $api->make_request( 'v1/notifications/webhooks', array(), array(), 'GET' );
 | |
| 		if ( ! empty( $response->webhooks ) && is_array( $response->webhooks ) ) {
 | |
| 			foreach ( $response->webhooks as $webhook ) {
 | |
| 				if ( ! empty( $webook->id ) && ! empty( $webhook->url ) && $webhook_url === $webhook->url ) {
 | |
| 					update_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ), sanitize_text_field( $webhook->id ) );
 | |
| 
 | |
| 					return true;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	} catch ( \Exception $e ) {
 | |
| 		// Continue to webhook creation.
 | |
| 	}
 | |
| 
 | |
| 	$event_types = array();
 | |
| 
 | |
| 	foreach ( array_keys( get_webhook_events( $mode ) ) as $event ) {
 | |
| 		$event_types[] = array( 'name' => $event );
 | |
| 	}
 | |
| 
 | |
| 	$response = $api->make_request( 'v1/notifications/webhooks', array(
 | |
| 		'url'         => $webhook_url,
 | |
| 		'event_types' => $event_types
 | |
| 	) );
 | |
| 
 | |
| 	if ( 201 !== $api->last_response_code ) {
 | |
| 		throw new API_Exception( sprintf(
 | |
| 		/* Translators: %d - HTTP response code; %s - Full response from the API. */
 | |
| 			__( 'Invalid response code %d while creating webhook. Response: %s', 'easy-digital-downloads' ),
 | |
| 			$api->last_response_code,
 | |
| 			json_encode( $response )
 | |
| 		) );
 | |
| 	}
 | |
| 
 | |
| 	if ( empty( $response->id ) ) {
 | |
| 		throw new API_Exception( __( 'Unexpected response from PayPal.', 'easy-digital-downloads' ) );
 | |
| 	}
 | |
| 
 | |
| 	update_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ), sanitize_text_field( $response->id ) );
 | |
| 
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Syncs the webhook with expected data. This replaces the webhook URL and event types with
 | |
|  * what EDD expects them to be. This can be used when the events need to be updated in
 | |
|  * the event that some are missing.
 | |
|  *
 | |
|  * @param string $mode Either `sandbox` or `live` mode. If omitted, current store mode is used.
 | |
|  *
 | |
|  * @since 2.11
 | |
|  * @return true
 | |
|  * @throws API_Exception
 | |
|  * @throws Authentication_Exception
 | |
|  */
 | |
| function sync_webhook( $mode = '' ) {
 | |
| 	if ( empty( $mode ) ) {
 | |
| 		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
 | |
| 	}
 | |
| 
 | |
| 	$webhook_id = get_webhook_id( $mode );
 | |
| 	if ( empty( $webhook_id ) ) {
 | |
| 		throw new \Exception( esc_html__( 'Webhook not configured.', 'easy-digital-downloads' ) );
 | |
| 	}
 | |
| 
 | |
| 	$event_types = array();
 | |
| 	foreach ( array_keys( get_webhook_events( $mode ) ) as $event ) {
 | |
| 		$event_types[] = array( 'name' => $event );
 | |
| 	}
 | |
| 
 | |
| 	$new_data = array(
 | |
| 		array(
 | |
| 			'op'    => 'replace',
 | |
| 			'path'  => '/url',
 | |
| 			'value' => get_webhook_url()
 | |
| 		),
 | |
| 		array(
 | |
| 			'op'    => 'replace',
 | |
| 			'path'  => '/event_types',
 | |
| 			'value' => $event_types
 | |
| 		)
 | |
| 	);
 | |
| 
 | |
| 	$api      = new API( $mode );
 | |
| 	$response = $api->make_request( 'v1/notifications/webhooks/' . urlencode( $webhook_id ), $new_data, array(), 'PATCH' );
 | |
| 	if ( 400 === $api->last_response_code && isset( $response->name ) && 'WEBHOOK_PATCH_REQUEST_NO_CHANGE' === $response->name ) {
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	if ( 200 !== $api->last_response_code ) {
 | |
| 		throw new API_Exception( sprintf(
 | |
| 		/* Translators: %d - HTTP response code; %s - Full response from the API. */
 | |
| 			__( 'Invalid response code %d while syncing webhook. Response: %s', 'easy-digital-downloads' ),
 | |
| 			$api->last_response_code,
 | |
| 			json_encode( $response )
 | |
| 		) );
 | |
| 	}
 | |
| 
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Retrieves information about the webhook EDD created.
 | |
|  *
 | |
|  * @param string $mode Mode to get the webhook in. If omitted, current store mode is used.
 | |
|  *
 | |
|  * @return object|false Webhook object on success, false if there is no webhook set up.
 | |
|  * @throws API_Exception
 | |
|  * @throws Authentication_Exception
 | |
|  */
 | |
| function get_webhook_details( $mode = '' ) {
 | |
| 	if ( empty( $mode ) ) {
 | |
| 		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
 | |
| 	}
 | |
| 
 | |
| 	$webhook_id = get_option( sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode ) );
 | |
| 
 | |
| 	// Bail if webhook was never set.
 | |
| 	if ( ! $webhook_id ) {
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	$api      = new API( $mode );
 | |
| 	$response = $api->make_request( 'v1/notifications/webhooks/' . urlencode( $webhook_id ), array(), array(), 'GET' );
 | |
| 	if ( 200 !== $api->last_response_code ) {
 | |
| 		if ( 404 === $api->last_response_code ) {
 | |
| 			throw new API_Exception(
 | |
| 				__( 'Your store is currently not receiving webhook notifications, create the webhooks to reconnect.', 'easy-digital-downloads' )
 | |
| 			);
 | |
| 		} else {
 | |
| 			throw new API_Exception( sprintf(
 | |
| 			/* Translators: %d - HTTP response code. */
 | |
| 				__( 'Invalid response code %d while retrieving webhook details.', 'easy-digital-downloads' ),
 | |
| 				$api->last_response_code
 | |
| 			) );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if ( empty( $response->id ) ) {
 | |
| 		throw new API_Exception( __( 'Unexpected response from PayPal when retrieving webhook details.', 'easy-digital-downloads' ) );
 | |
| 	}
 | |
| 
 | |
| 	return $response;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Deletes the webhook.
 | |
|  *
 | |
|  * @since 2.11
 | |
|  *
 | |
|  * @param string $mode
 | |
|  *
 | |
|  * @throws API_Exception
 | |
|  * @throws Authentication_Exception
 | |
|  */
 | |
| function delete_webhook( $mode = '' ) {
 | |
| 	if ( empty( $mode ) ) {
 | |
| 		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
 | |
| 	}
 | |
| 
 | |
| 	$webhook_name = sanitize_key( 'edd_paypal_commerce_webhook_id_' . $mode );
 | |
| 	$webhook_id   = get_option( $webhook_name );
 | |
| 
 | |
| 	// Bail if webhook was never set.
 | |
| 	if ( ! $webhook_id ) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	$api = new API( $mode );
 | |
| 
 | |
| 	$api->make_request( 'v1/notifications/webhooks/' . urlencode( $webhook_id ), array(), array(), 'DELETE' );
 | |
| 
 | |
| 	if ( 204 !== $api->last_response_code ) {
 | |
| 		throw new API_Exception( sprintf(
 | |
| 		/* Translators: %d - HTTP response code. */
 | |
| 			__( 'Invalid response code %d while deleting webhook.', 'easy-digital-downloads' ),
 | |
| 			$api->last_response_code
 | |
| 		) );
 | |
| 	}
 | |
| }
 |