502 lines
14 KiB
PHP
502 lines
14 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* WooCommerce API Webhooks class
|
||
|
*
|
||
|
* Handles requests to the /webhooks endpoint
|
||
|
*
|
||
|
* @author WooThemes
|
||
|
* @category API
|
||
|
* @package WooCommerce/API
|
||
|
* @since 2.2
|
||
|
*/
|
||
|
|
||
|
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
|
||
|
|
||
|
class WC_API_Webhooks extends WC_API_Resource {
|
||
|
|
||
|
/** @var string $base the route base */
|
||
|
protected $base = '/webhooks';
|
||
|
|
||
|
/**
|
||
|
* Register the routes for this class
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param array $routes
|
||
|
* @return array
|
||
|
*/
|
||
|
public function register_routes( $routes ) {
|
||
|
|
||
|
# GET|POST /webhooks
|
||
|
$routes[ $this->base ] = array(
|
||
|
array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ),
|
||
|
array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||
|
);
|
||
|
|
||
|
# GET /webhooks/count
|
||
|
$routes[ $this->base . '/count'] = array(
|
||
|
array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
|
||
|
);
|
||
|
|
||
|
# GET|PUT|DELETE /webhooks/<id>
|
||
|
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||
|
array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ),
|
||
|
array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||
|
array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
|
||
|
);
|
||
|
|
||
|
# GET /webhooks/<id>/deliveries
|
||
|
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
|
||
|
array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
|
||
|
);
|
||
|
|
||
|
# GET /webhooks/<webhook_id>/deliveries/<id>
|
||
|
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
|
||
|
array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
|
||
|
);
|
||
|
|
||
|
return $routes;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all webhooks
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param string $fields
|
||
|
* @param array $filter
|
||
|
* @param int $page
|
||
|
* @return array
|
||
|
*/
|
||
|
public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {
|
||
|
|
||
|
if ( ! empty( $status ) ) {
|
||
|
$filter['status'] = $status;
|
||
|
}
|
||
|
|
||
|
$filter['page'] = $page;
|
||
|
|
||
|
$query = $this->query_webhooks( $filter );
|
||
|
|
||
|
$webhooks = array();
|
||
|
|
||
|
foreach( $query->posts as $webhook_id ) {
|
||
|
|
||
|
if ( ! $this->is_readable( $webhook_id ) ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
|
||
|
}
|
||
|
|
||
|
$this->server->add_pagination_headers( $query );
|
||
|
|
||
|
return array( 'webhooks' => $webhooks );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the webhook for the given ID
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param int $id webhook ID
|
||
|
* @param array $fields
|
||
|
* @return array
|
||
|
*/
|
||
|
public function get_webhook( $id, $fields = null ) {
|
||
|
|
||
|
// ensure webhook ID is valid & user has permission to read
|
||
|
$id = $this->validate_request( $id, 'shop_webhook', 'read' );
|
||
|
|
||
|
if ( is_wp_error( $id ) ) {
|
||
|
return $id;
|
||
|
}
|
||
|
|
||
|
$webhook = new WC_Webhook( $id );
|
||
|
|
||
|
$webhook_data = array(
|
||
|
'id' => $webhook->id,
|
||
|
'name' => $webhook->get_name(),
|
||
|
'status' => $webhook->get_status(),
|
||
|
'topic' => $webhook->get_topic(),
|
||
|
'resource' => $webhook->get_resource(),
|
||
|
'event' => $webhook->get_event(),
|
||
|
'hooks' => $webhook->get_hooks(),
|
||
|
'delivery_url' => $webhook->get_delivery_url(),
|
||
|
'created_at' => $this->server->format_datetime( $webhook->get_post_data()->post_date_gmt ),
|
||
|
'updated_at' => $this->server->format_datetime( $webhook->get_post_data()->post_modified_gmt ),
|
||
|
);
|
||
|
|
||
|
return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the total number of webhooks
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param string $status
|
||
|
* @param array $filter
|
||
|
* @return array
|
||
|
*/
|
||
|
public function get_webhooks_count( $status = null, $filter = array() ) {
|
||
|
|
||
|
if ( ! empty( $status ) ) {
|
||
|
$filter['status'] = $status;
|
||
|
}
|
||
|
|
||
|
$query = $this->query_webhooks( $filter );
|
||
|
|
||
|
if ( ! current_user_can( 'read_private_shop_webhooks' ) ) {
|
||
|
return new WP_Error( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), array( 'status' => 401 ) );
|
||
|
}
|
||
|
|
||
|
return array( 'count' => (int) $query->found_posts );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create an webhook
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param array $data parsed webhook data
|
||
|
* @return array
|
||
|
*/
|
||
|
public function create_webhook( $data ) {
|
||
|
|
||
|
$data = isset( $data['webhook'] ) ? $data['webhook'] : array();
|
||
|
|
||
|
try {
|
||
|
|
||
|
// permission check
|
||
|
if ( ! current_user_can( 'publish_shop_webhooks' ) ) {
|
||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks', 'woocommerce' ), 401 );
|
||
|
}
|
||
|
|
||
|
$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );
|
||
|
|
||
|
// validate topic
|
||
|
if ( empty( $data['topic'] ) || ! $this->is_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid', 'woocommerce' ), 400 );
|
||
|
}
|
||
|
|
||
|
// validate delivery URL
|
||
|
if ( empty( $data['delivery_url'] ) || ! $this->is_valid_url( $data['delivery_url'] ) ) {
|
||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||
|
}
|
||
|
|
||
|
$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
|
||
|
'post_type' => 'shop_webhook',
|
||
|
'post_status' => 'publish',
|
||
|
'ping_status' => 'closed',
|
||
|
'post_author' => get_current_user_id(),
|
||
|
'post_password' => uniqid( 'webhook_' ),
|
||
|
'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ),
|
||
|
), $data, $this );
|
||
|
|
||
|
$webhook_id = wp_insert_post( $webhook_data );
|
||
|
|
||
|
if ( is_wp_error( $webhook_id ) || ! $webhook_id ) {
|
||
|
throw new WC_API_Exception( 'woocommerce_api_cannot_create_webhook', sprintf( __( 'Cannot create webhook: %s', 'woocommerce' ), is_wp_error( $webhook_id ) ? implode( ', ', $webhook_id->get_error_messages() ) : '0' ), 500 );
|
||
|
}
|
||
|
|
||
|
$webhook = new WC_Webhook( $webhook_id );
|
||
|
|
||
|
// set topic, delivery URL, and optional secret
|
||
|
$webhook->set_topic( $data['topic'] );
|
||
|
$webhook->set_delivery_url( $data['delivery_url'] );
|
||
|
|
||
|
// set secret if provided, defaults to API users consumer secret
|
||
|
$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : get_user_meta( get_current_user_id(), 'woocommerce_api_consumer_secret', true ) );
|
||
|
|
||
|
// send ping
|
||
|
$webhook->deliver_ping();
|
||
|
|
||
|
// HTTP 201 Created
|
||
|
$this->server->send_status( 201 );
|
||
|
|
||
|
do_action( 'woocommerce_api_create_webhook', $webhook->id, $this );
|
||
|
|
||
|
return $this->get_webhook( $webhook->id );
|
||
|
|
||
|
} catch ( WC_API_Exception $e ) {
|
||
|
|
||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Edit a webhook
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param int $id webhook ID
|
||
|
* @param array $data parsed webhook data
|
||
|
* @return array
|
||
|
*/
|
||
|
public function edit_webhook( $id, $data ) {
|
||
|
|
||
|
$data = isset( $data['webhook'] ) ? $data['webhook'] : array();
|
||
|
|
||
|
try {
|
||
|
|
||
|
$id = $this->validate_request( $id, 'shop_webhook', 'edit' );
|
||
|
|
||
|
if ( is_wp_error( $id ) ) {
|
||
|
return $id;
|
||
|
}
|
||
|
|
||
|
$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );
|
||
|
|
||
|
$webhook = new WC_Webhook( $id );
|
||
|
|
||
|
// update topic
|
||
|
if ( ! empty( $data['topic'] ) ) {
|
||
|
|
||
|
if ( $this->is_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||
|
|
||
|
$webhook->set_topic( $data['topic'] );
|
||
|
|
||
|
} else {
|
||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid', 'woocommerce' ), 400 );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// update delivery URL
|
||
|
if ( ! empty( $data['delivery_url'] ) ) {
|
||
|
if ( $this->is_valid_url( $data['delivery_url'] ) ) {
|
||
|
|
||
|
$webhook->set_delivery_url( $data['delivery_url'] );
|
||
|
|
||
|
} else {
|
||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// update secret
|
||
|
if ( ! empty( $data['secret'] ) ) {
|
||
|
$webhook->set_secret( $data['secret'] );
|
||
|
}
|
||
|
|
||
|
// update status
|
||
|
if ( ! empty( $data['status'] ) ) {
|
||
|
$webhook->update_status( $data['status'] );
|
||
|
}
|
||
|
|
||
|
// update user ID
|
||
|
$webhook_data = array(
|
||
|
'ID' => $webhook->id,
|
||
|
'post_author' => get_current_user_id()
|
||
|
);
|
||
|
|
||
|
// update name
|
||
|
if ( ! empty( $data['name'] ) ) {
|
||
|
$webhook_data['post_title'] = $data['name'];
|
||
|
}
|
||
|
|
||
|
// update post
|
||
|
wp_update_post( $webhook_data );
|
||
|
|
||
|
do_action( 'woocommerce_api_edit_webhook', $webhook->id, $this );
|
||
|
|
||
|
return $this->get_webhook( $id );
|
||
|
|
||
|
} catch ( WC_API_Exception $e ) {
|
||
|
|
||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Check if the given topic is a valid webhook topic, a topic is valid if:
|
||
|
*
|
||
|
* + starts with `action.woocommerce_` or `action.wc_`
|
||
|
* + it has a valid resource & event
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param string $topic webhook topic
|
||
|
* @return bool true if valid, false otherwise
|
||
|
*/
|
||
|
private function is_valid_topic( $topic ) {
|
||
|
|
||
|
// custom topics are prefixed with woocommerce_ or wc_ are valid
|
||
|
if ( 0 === strpos( $topic, 'action.woocommerce_' ) || 0 === strpos( $topic, 'action.wc_' ) ) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
list( $resource, $event ) = explode( '.', $topic );
|
||
|
|
||
|
if ( ! isset( $resource ) || ! isset( $event ) ) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) );
|
||
|
$valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted' ) );
|
||
|
|
||
|
if ( in_array( $resource, $valid_resources ) && in_array( $event, $valid_events ) ) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Simple check for validating a URL, it must start with http:// or https://
|
||
|
* and pass FILTER_VALIDATE_URL validation
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param string $url delivery URL for the webhook
|
||
|
* @return bool true if valid, false otherwise
|
||
|
*/
|
||
|
private function is_valid_url( $url ) {
|
||
|
|
||
|
// must start with http:// or https://
|
||
|
if ( 0 !== strpos( $url, 'http://' ) && 0 !== strpos( $url, 'https://' ) ) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// must pass validation
|
||
|
if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Delete a webhook
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param int $id webhook ID
|
||
|
* @return array
|
||
|
*/
|
||
|
public function delete_webhook( $id ) {
|
||
|
|
||
|
$id = $this->validate_request( $id, 'shop_webhook', 'delete' );
|
||
|
|
||
|
if ( is_wp_error( $id ) ) {
|
||
|
return $id;
|
||
|
}
|
||
|
|
||
|
do_action( 'woocommerce_api_delete_webhook', $id, $this );
|
||
|
|
||
|
// no way to manage trashed webhooks at the moment, so force delete
|
||
|
return $this->delete( $id, 'webhook', true );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper method to get webhook post objects
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param array $args request arguments for filtering query
|
||
|
* @return WP_Query
|
||
|
*/
|
||
|
private function query_webhooks( $args ) {
|
||
|
|
||
|
// set base query arguments
|
||
|
$query_args = array(
|
||
|
'fields' => 'ids',
|
||
|
'post_type' => 'shop_webhook',
|
||
|
);
|
||
|
|
||
|
// add status argument
|
||
|
if ( ! empty( $args['status'] ) ) {
|
||
|
|
||
|
switch ( $args['status'] ) {
|
||
|
case 'active':
|
||
|
$query_args['post_status'] = 'publish';
|
||
|
break;
|
||
|
case 'paused':
|
||
|
$query_args['post_status'] = 'draft';
|
||
|
break;
|
||
|
case 'disabled':
|
||
|
$query_args['post_status'] = 'pending';
|
||
|
break;
|
||
|
default:
|
||
|
$query_args['post_status'] = 'publish';
|
||
|
}
|
||
|
unset( $args['status'] );
|
||
|
}
|
||
|
|
||
|
$query_args = $this->merge_query_args( $query_args, $args );
|
||
|
|
||
|
return new WP_Query( $query_args );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get deliveries for a webhook
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param string $webhook_id webhook ID
|
||
|
* @param string|null $fields fields to include in response
|
||
|
* @return array
|
||
|
*/
|
||
|
public function get_webhook_deliveries( $webhook_id, $fields = null ) {
|
||
|
|
||
|
// ensure ID is valid webhook ID
|
||
|
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||
|
|
||
|
if ( is_wp_error( $webhook_id ) ) {
|
||
|
return $webhook_id;
|
||
|
}
|
||
|
|
||
|
$webhook = new WC_Webhook( $webhook_id );
|
||
|
|
||
|
$logs = $webhook->get_delivery_logs();
|
||
|
|
||
|
$delivery_logs = array();
|
||
|
|
||
|
foreach ( $logs as $log ) {
|
||
|
|
||
|
// add timestamp
|
||
|
$log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt );
|
||
|
|
||
|
// remove comment object
|
||
|
unset( $log['comment'] );
|
||
|
|
||
|
$delivery_logs[] = $log;
|
||
|
}
|
||
|
|
||
|
return array( 'webhook_deliveries' => $delivery_logs );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the delivery log for the given webhook ID and delivery ID
|
||
|
*
|
||
|
* @since 2.2
|
||
|
* @param string $webhook_id webhook ID
|
||
|
* @param string $id delivery log ID
|
||
|
* @param string|null $fields fields to limit response to
|
||
|
* @return array
|
||
|
*/
|
||
|
public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
|
||
|
|
||
|
// validate webhook ID
|
||
|
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||
|
|
||
|
if ( is_wp_error( $webhook_id ) ) {
|
||
|
return $webhook_id;
|
||
|
}
|
||
|
|
||
|
$id = absint( $id );
|
||
|
|
||
|
if ( empty( $id ) ) {
|
||
|
return new WP_Error( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID', 'woocommerce' ), array( 'status' => 400 ) );
|
||
|
}
|
||
|
|
||
|
$webhook = new WC_Webhook( $webhook_id );
|
||
|
|
||
|
$log = $webhook->get_delivery_log( $id );
|
||
|
|
||
|
if ( ! $log ) {
|
||
|
return new WP_Error( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery', 'woocommerce' ), array( 'status' => 400 ) );
|
||
|
}
|
||
|
|
||
|
$delivery_log = $log;
|
||
|
|
||
|
// add timestamp
|
||
|
$delivery_log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt );
|
||
|
|
||
|
// remove comment object
|
||
|
unset( $delivery_log['comment'] );
|
||
|
|
||
|
return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', $delivery_log, $id, $fields, $log, $webhook_id, $this ) );
|
||
|
}
|
||
|
|
||
|
}
|