LLMS_REST_Webhook
LLMS_REST_Webhook class
Source Source
File: libraries/lifterlms-rest/includes/models/class-llms-rest-webhook.php
class LLMS_REST_Webhook extends LLMS_REST_Webhook_Data { /** * Store which object IDs this webhook has processed (ie scheduled to be delivered) * within the current page request. * * @var array */ protected $processed = array(); /** * Delivers the webhook * * @since 1.0.0-beta.1 * * @param array $args Numeric array of arguments from the originating hook. * @return void */ public function deliver( $args ) { $start = microtime( true ); $payload = $this->get_payload( $args ); $http_args = array( 'method' => 'POST', 'timeout' => 60, 'redirection' => 0, 'user-agent' => $this->get_user_agent(), 'body' => trim( wp_json_encode( $payload ) ), 'headers' => array( 'Content-Type' => 'application/json', ), ); /** * Modify HTTP args used to deliver the webhook * * @since 1.0.0-beta.1 * * @param array $http_args HTTP request args suitable for `wp_remote_request()`. * @param LLMS_REST_Webhook $this Webhook object. * @param mixed $args First argument passed to the action triggering the webhook. */ $http_args = apply_filters( 'llms_rest_webhook_delivery_args', $http_args, $this, $args ); $delivery_id = wp_hash( $this->get( 'id' ) . strtotime( 'now' ) ); $http_args['headers'] = array_merge( $http_args['headers'], array( 'X-LLMS-Webhook-Source' => home_url( '/' ), 'X-LLMS-Webhook-Topic' => $this->get( 'topic' ), 'X-LLMS-Webhook-Resource' => $this->get_resource(), 'X-LLMS-Webhook-Event' => $this->get_event(), 'X-LLMS-Webhook-Signature' => $this->get_delivery_signature( $http_args['body'] ), 'X-LLMS-Webhook-ID' => $this->get( 'id' ), 'X-LLMS-Delivery-ID' => $delivery_id, ) ); $res = wp_safe_remote_request( $this->get( 'delivery_url' ), $http_args ); $duration = round( microtime( true ) - $start, 5 ); $this->delivery_after( $delivery_id, $http_args, $res, $duration ); /** * Fires after a webhook is delivered * * @since 1.0.0-beta.1 * * @param array $http_args HTTP request args. * @param WP_Error|array $res Remote response. * @param int $duration Executing time. * @param array $args Numeric array of arguments from the originating hook. * @param LLMS_REST_Webhook $this Webhook object. */ do_action( 'llms_rest_webhook_delivery', $http_args, $res, $duration, $args, $this ); } /** * Fires after delivery * * Logs data when loggind enabled and updates state data. * * @since 1.0.0-beta.1 * @since 1.0.0-beta.17 Stop setting the webhook's property `pending_delivery` to 0. * We now rely on the method `is_already_processed()` to determine whether the webhook delivering should be avoided. * * @param string $delivery_id Webhook delivery id (for logging). * @param array $req_args HTTP Request Arguments used to deliver the webhook. * @param array $res Results from `wp_safe_remote_request()`. * @param float $duration Time (in microseconds) it took to generate and deliver the webhook. * @return void */ protected function delivery_after( $delivery_id, $req_args, $res, $duration ) { // Parse response. if ( is_wp_error( $res ) ) { $res_code = $res->get_error_code(); $res_message = $res->get_error_message(); $res_headers = array(); $res_body = ''; } else { $res_code = wp_remote_retrieve_response_code( $res ); $res_message = wp_remote_retrieve_response_message( $res ); $res_headers = wp_remote_retrieve_headers( $res ); $res_body = wp_remote_retrieve_body( $res ); } if ( defined( 'LLMS_REST_WEBHOOK_DELIVERY_LOGGING' ) && LLMS_REST_WEBHOOK_DELIVERY_LOGGING ) { $message = array( 'Delivery ID' => $delivery_id, 'Date' => date_i18n( __( 'M j, Y @ H:i', 'lifterlms' ), strtotime( 'now' ), true ), 'URL' => $this->get( 'delivery_url' ), 'Duration' => $duration, 'Request' => array( 'Method' => $req_args['method'], 'Headers' => array_merge( array( 'User-Agent' => $req_args['user-agent'], ), $req_args['headers'] ), ), 'Body' => wp_slash( $req_args['body'] ), 'Response' => array( 'Code' => $res_code, 'Message' => $res_message, 'Headers' => $res_headers, 'Body' => $res_body, ), ); if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { $message['Webhook Delivery']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on.'; $message['Webhook Delivery']['Response']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on.'; } llms_log( $message, sprintf( 'webhook-%d', $this->get( 'id' ) ) ); } // Check for a success, which is a 2xx, 301 or 302 Response Code. if ( absint( $res_code ) >= 200 && absint( $res_code ) <= 302 ) { $this->set( 'failure_count', 0 ); } else { $this->set_delivery_failure(); } } /** * Add actions for all the webhooks hooks * * @since 1.0.0-beta.1 * * @return void */ public function enqueue() { foreach ( $this->get_hooks() as $hook => $args ) { add_action( $hook, array( $this, 'process_hook' ), 10, $args ); } } /** * Checks if the specified resource has already been queued for delivery within the current request * * Helps avoid duplication of data being sent for topics that have more than one hook defined. * * @param array $args Numeric array of arguments from the originating hook. * @return bool */ protected function is_already_processed( $args ) { return false !== array_search( $args[0], $this->processed, true ); } /** * Determine if the current action is valid for the webhook * * @since 1.0.0-beta.1 * * @param array $args Numeric array of arguments from the originating hook. * @return bool */ protected function is_valid_action( $args ) { $ret = true; switch ( current_action() ) { case 'wp_trash_post': case 'delete_post': case 'untrashed_post': $ret = $this->is_valid_post_action( $args[0] ); break; case 'user_register': case 'profile_update': case 'delete_user': $ret = $this->is_valid_user_action( $args[0] ); break; } /** * Determine if the current action is valid for the webhook * * @param bool $ret Whether or not the action is valid. * @param array $args Numeric array of arguments from the originating hook. * @param LLMS_REST_Webhook $this Webhook object. */ return apply_filters( 'llms_rest_webhook_is_valid_action', $ret, $args, $this ); } /** * Determine if the current post-related action is valid for the webhook * * @since 1.0.0-beta.1 * * @param int $post_id WP Post ID. * @return bool */ protected function is_valid_post_action( $post_id ) { $post_type = get_post_type( $post_id ); // Check the post type is a supported post type. if ( ! in_array( get_post_type( $post_id ), LLMS_REST_API()->webhooks()->get_post_type_resources(), true ) ) { return false; } // Ensure the current action matches the resource for the current webhook. if ( str_replace( 'llms_', '', $post_type ) !== $this->get_resource() ) { return false; } return true; } /** * Determine if the the resource is valid for the webhook * * @since 1.0.0-beta.1 * @since 1.0.0-beta.11 Skipped autosaves and revisions. * Implemented a new way to consider a resource as just created. Thanks WooCoommerce. * * @param array $args Numeric array of arguments from the originating hook. * @return bool */ protected function is_valid_resource( $args ) { $resource = $this->get_resource(); if ( in_array( $resource, LLMS_REST_API()->webhooks()->get_post_type_resources(), true ) ) { $post_resource = get_post( absint( $args[0] ) ); // Ignore auto-drafts. if ( in_array( get_post_status( $post_resource ), array( 'new', 'auto-draft' ), true ) ) { return false; } if ( false !== strpos( current_action(), 'save_post' ) || false !== strpos( current_action(), 'edit_post' ) ) { // Ignore autosaves and revisions. if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || is_int( wp_is_post_revision( $post_resource ) ) || is_int( wp_is_post_autosave( $resource ) ) ) { return false; } // Drafts don't have post_date_gmt so calculate it here. $gmt_date = get_gmt_from_date( $post_resource->post_date ); // A resource is considered created when the hook is executed within 10 seconds of the post creation date. $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) ); $event = $this->get_event(); if ( ( 'created' === $event && false !== strpos( current_action(), 'save_post' ) ) && ! $resource_created ) { return false; } elseif ( ( 'updated' === $event && false !== strpos( current_action(), 'edit_post' ) ) && $resource_created ) { return false; } } } return true; } /** * Determine if the current user-related action is valid for the webhook * * @since 1.0.0-beta.1 * * @param int $user_id WP User ID. * @return bool */ protected function is_valid_user_action( $user_id ) { $user = get_userdata( $user_id ); if ( ! $user ) { return false; } $resource = $this->get_resource(); if ( 'student' === $resource && ! in_array( 'student', (array) $user->roles, true ) ) { return false; } elseif ( 'instructor' === $resource && ! user_can( $user_id, 'lifterlms_instructor' ) ) { return false; } return true; } /** * Processes information from the origination action hook * * Determines if the webhook should be delivered and whether or not it should be scheduled or delivered immediately. * * @since 1.0.0-beta.1 * @since 1.0.0-beta.17 Mark this hook's first argument as processed to ensure it doesn't get processed again within the current request. * And don't rely anymore on the webhook's `pending_delivery` property to achieve the same goal. * * @param mixed ...$args Arguments from the hook. * @return int|false Timestamp of the scheduled event when the webhook is successfully scheduled. * false if the webhook should not be delivered or has already been delivered in the last 5 minutes. */ public function process_hook( ...$args ) { if ( ! $this->should_deliver( $args ) ) { return false; } // Mark this hook's first argument as processed to ensure it doesn't get processed again within the current request, // as it might happen with webhooks with multiple hookes defined in `LLMS_REST_Webhooks::get_hooks()`. $this->processed[] = $args[0]; /** * Disable background processing of webhooks by returning a falsy * * Note: disabling async processing may create delays for users of your site. * * @param bool $async Whether async processing is enabled or not. * @param LLMS_REST_Webhook $this Webhook object. * @param array $args Numeric array of arguments from the originating hook. */ if ( apply_filters( 'llms_rest_webhook_deliver_async', true, $this, $args ) ) { return $this->schedule( $args ); } return $this->deliver( $args ); } /** * Perform a test ping to the delivery url * * @since 1.0.0-beta.1 * * @return true|WP_Error */ public function ping() { $pre = apply_filters( 'llms_rest_webhook_pre_ping', false, $this->get( 'id' ) ); if ( false !== $pre ) { return $pre; } $ping = wp_safe_remote_post( $this->get( 'delivery_url' ), array( 'user-agent' => $this->get_user_agent(), 'body' => sprintf( 'webhook_id=%d', $this->get( 'id' ) ), ) ); $res_code = wp_remote_retrieve_response_code( $ping ); if ( is_wp_error( $ping ) ) { // Translators: %s = Error message. return new WP_Error( 'llms_rest_webhook_ping_unreachable', sprintf( __( 'Could not reach the delivery url: "%s".', 'lifterlms' ), $ping->get_error_message() ) ); } if ( 200 !== $res_code ) { // Translators: %d = Response code. return new WP_Error( 'llms_rest_webhook_ping_not_200', sprintf( __( 'The delivery url returned the response code "%d".', 'lifterlms' ), absint( $res_code ) ) ); } return true; } /** * Determines if an originating action qualifies for webhook delivery * * @since 1.0.0-beta.1 * @since [verison] Drop checking whether the webhook is pending in favor of a check on if is already processed within the current request. * * @param array $args Numeric array of arguments from the originating hook. * @return bool */ protected function should_deliver( $args ) { $deliver = ( 'active' === $this->get( 'status' ) ) // Must be active. && $this->is_valid_action( $args ) // Valid action. && $this->is_valid_resource( $args ) // Valid resource. && ! $this->is_already_processed( $args ); // Not already processed. /** * Skip or hijack webhook delivery scheduling * * @param bool $deliver Whether or not to deliver webhook delivery. * @param LLMS_REST_Webhook $this Webhook object. * @param array $args Numeric array of arguments from the originating hook. */ return apply_filters( 'llms_rest_webhook_should_deliver', $deliver, $this, $args ); } /** * Schedule the webhook for async delivery * * @since 1.0.0-beta.1 * @since 1.0.0-beta.17 Stop setting the webhook's property `pending_delivery` to 1 when scheduling the delivery. * We now rely on the method `is_already_processed()` to determine whether the webhook scheduling should be avoided. * * @param array $args Numeric array of arguments from the originating hook. * @return bool */ protected function schedule( $args ) { // Remove object & array arguments before scheduling to avoid hitting column index size issues imposed by the ActionScheduler lib. foreach ( $args as $index => &$arg ) { if ( is_array( $arg ) || is_object( $arg ) ) { $arg = null; } } $schedule_args = array( 'webhook_id' => $this->get( 'id' ), 'args' => $args, ); $next = as_next_scheduled_action( 'lifterlms_rest_deliver_webhook_async', $schedule_args, 'llms-webhooks' ); /** * Determines the time period required to wait between delivery of the webhook * * If the webhook has already been scheduled within this time period it will not be sent again * until the period expires. For example, the default time period is 300 seconds (5 minutes). * If the webhook is triggered at 12:00pm it will be scheduled. If it is triggered again at 12:03pm the * second occurrence will not be scheduled. If it is triggerd again at 12:06pm this third occurrence will * again be scheduled. * * @since 1.0.0-beta.1 * * @param int $delay Time (in seconds). * @param array $args Numeric array of arguments from the originating hook. * @param LLMS_REST_Webhook $this Webhook object. */ $delay = apply_filters( 'llms_rest_webhook_repeat_delay', 300, $args, $this ); if ( ! $next || $next >= ( $delay + gmdate( 'U' ) ) ) { return as_schedule_single_action( time(), 'lifterlms_rest_deliver_webhook_async', $schedule_args, 'llms-webhooks' ) ? true : false; } return false; } }
Expand full source code Collapse full source code View on GitHub
Methods Methods
- deliver — Delivers the webhook
- delivery_after — Fires after delivery
- enqueue — Add actions for all the webhooks hooks
- is_already_processed — Checks if the specified resource has already been queued for delivery within the current request
- is_pending — Determine if the webhook is currently pending delivery
- is_valid_action — Determine if the current action is valid for the webhook
- is_valid_post_action — Determine if the current post-related action is valid for the webhook
- is_valid_resource — Determine if the the resource is valid for the webhook
- is_valid_user_action — Determine if the current user-related action is valid for the webhook
- ping — Perform a test ping to the delivery url
- process_hook — Processes information from the origination action hook
- schedule — Schedule the webhook for async delivery
- should_deliver — Determines if an originating action qualifies for webhook delivery
Changelog Changelog
Version | Description |
---|---|
1.0.0-beta.11 | When validating a resource:
|
1.0.0-beta.1 | Introduced. |