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/ $routes[ $this->base . '/(?P\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//deliveries $routes[ $this->base . '/(?P\d+)/deliveries' ] = array( array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), ); # GET /webhooks//deliveries/ $routes[ $this->base . '/(?P\d+)/deliveries/(?P\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' => 404 ) ); } $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 ) ); } }