> */ public function data() { return array_merge( parent::data(), array( Cleanup::record( 'webhooks', __( 'Webhook changes', 'admin-powerups-for-woocommerce' ), __( 'Webhooks you re-enable stay active in WooCommerce, and a successful test ping stays recorded there. They stay when the power-up is off or removed.', 'admin-powerups-for-woocommerce' ) ), ) ); } /** * {@inheritDoc} Only REST routes; nothing else runs on any request. */ public function boot() { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } /** * {@inheritDoc} */ public function views() { return array( 'webhooks' => __( 'Webhooks', 'admin-powerups-for-woocommerce' ) ); } /** * {@inheritDoc} */ public function shortcut() { return 'webhooks'; } /** * Webhooks tab URL. * * @since 1.0.0 * @return string */ public static function url() { return AdminPage::url( 'webhook-health', 'webhooks' ); } /** * WooCommerce's log screen filtered to webhook deliveries, or '' when unavailable. * * @since 1.0.0 * @return string */ public static function logs_url() { try { return add_query_arg( 'source', Deliveries::SOURCE, LoggingUtil::get_logs_tab_url() ); } catch ( \Throwable $e ) { return admin_url( 'admin.php?page=wc-status&tab=logs' ); } } /** * {@inheritDoc} * * @since 1.0.0 * @param string $view View ID. */ public function render_view( $view ) { $rows = Webhooks::rows(); $counts = Webhooks::counts(); $total = array_sum( $counts ); $logs_url = self::logs_url(); include __DIR__ . '/views/webhooks.php'; } /** * One webhook row's HTML. * * @since 1.0.0 * @param array $row Row from Webhooks::row(). * @return string Markup, for REST responses. */ public function row_html( array $row ) { ob_start(); $this->render_row( $row ); return (string) ob_get_clean(); } /** * Print one webhook row's HTML. * * @since 1.0.0 * @param array $row Row from Webhooks::row(). * @return void */ public function render_row( array $row ) { include __DIR__ . '/views/row.php'; } /** * {@inheritDoc} * * @since 1.0.0 * @param string $view View ID. */ public function enqueue_view_assets( $view ) { wp_enqueue_style( 'apwc-webhook-health', Assets::url( 'assets/css/webhook-health.css' ), array( 'apwc-admin' ), Assets::ver( 'assets/css/webhook-health.css' ) ); wp_enqueue_script( 'apwc-webhook-health', Assets::url( 'assets/js/webhook-health.js' ), array( 'wp-api-fetch', 'wp-i18n', 'apwc-admin' ), Assets::ver( 'assets/js/webhook-health.js' ), true ); wp_set_script_translations( 'apwc-webhook-health', 'admin-powerups-for-woocommerce' ); } /* ------------------------------------------------------------------ REST */ /** * REST routes (registered only while the power-up is on). * * - GET /webhook-health/deliveries Safe * - POST /webhook-health/{id}/test Safe * - POST /webhook-health/{id}/enable Review * * @since 1.0.0 * @return void */ public function register_routes() { $id = array( 'id' => array( 'type' => 'integer', 'minimum' => 1, 'required' => true, ), ); register_rest_route( 'apwc/v1', '/webhook-health/deliveries', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'rest_deliveries' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::SAFE, 'manage_woocommerce' ), 'args' => array( 'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 200, ), ), ) ); register_rest_route( 'apwc/v1', '/webhook-health/(?P\d+)/test', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_test' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::SAFE, 'manage_woocommerce' ), 'args' => $id, ) ); register_rest_route( 'apwc/v1', '/webhook-health/(?P\d+)/enable', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_enable' ), 'permission_callback' => DevGuard::permission( $this, DevGuard::REVIEW, 'manage_woocommerce' ), 'args' => $id, ) ); } /** * GET /webhook-health/deliveries: recent deliveries (HTML), the last delivery per webhook and * the delivery queue. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response */ public function rest_deliveries( WP_REST_Request $request ) { $limit = null === $request['limit'] ? Deliveries::default_limit() : (int) $request['limit']; $result = Deliveries::read( $limit ); $queue = Deliveries::queue(); $names = array(); $last = array(); $logs_url = self::logs_url(); foreach ( $result['last'] as $id => $entry ) { $last[ $id ] = array( 'ok' => LogParser::is_success( (string) $entry['code'] ), 'code' => (string) $entry['code'], 'when' => $this->ago( (int) $entry['time'] ), ); } foreach ( $result['rows'] as $entry ) { $id = (int) $entry['webhook_id']; if ( ! isset( $names[ $id ] ) ) { $webhook = Webhooks::get( $id ); // Once per webhook shown (deleted ones show their ID). $names[ $id ] = $webhook ? (string) $webhook->get_name( 'edit' ) : ''; } } ob_start(); include __DIR__ . '/views/deliveries.php'; $html = (string) ob_get_clean(); return rest_ensure_response( array( 'html' => $html, 'state' => $result['state'], 'count' => count( $result['rows'] ), 'unparsed' => (int) $result['unparsed'], 'last' => (object) $last, 'queue' => $queue, ) ); } /** * POST /webhook-health/{id}/test: WooCommerce's test ping. * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_test( WP_REST_Request $request ) { $result = Webhooks::test( (int) $request['id'] ); if ( is_wp_error( $result ) ) { return $result; } $row = Webhooks::row( $result['webhook'] ); $host = '' !== $row['host'] ? $row['host'] : __( 'the delivery URL', 'admin-powerups-for-woocommerce' ); $code = (int) $result['code']; if ( $result['ok'] ) { /* translators: 1: host name, 2: milliseconds. */ $message = sprintf( __( 'Test ping to %1$s: 200 OK in %2$s ms.', 'admin-powerups-for-woocommerce' ), $host, number_format_i18n( (int) $result['ms'] ) ); } elseif ( $code >= 200 && $code < 303 ) { /* translators: 1: host name, 2: HTTP status code. */ $message = sprintf( __( 'Test ping to %1$s got %2$d. WooCommerce’s test ping only accepts 200; real deliveries accept 2xx–302.', 'admin-powerups-for-woocommerce' ), $host, $code ); } elseif ( $code > 0 ) { /* translators: 1: host name, 2: HTTP status code. */ $message = sprintf( __( 'Test ping to %1$s got %2$d.', 'admin-powerups-for-woocommerce' ), $host, $code ); } else { $message = sprintf( /* translators: 1: host name, 2: error message. */ __( 'Test ping to %1$s failed: %2$s', 'admin-powerups-for-woocommerce' ), $host, '' !== $result['error'] ? $result['error'] : __( 'no answer.', 'admin-powerups-for-woocommerce' ) ); } return rest_ensure_response( array( 'row_html' => $this->row_html( $row ), 'message' => $message, 'ok' => (bool) $result['ok'], 'code' => $code, 'ms' => (int) $result['ms'], ) ); } /** * POST /webhook-health/{id}/enable: re-enable a disabled webhook (409 for any other status). * * @since 1.0.0 * @param WP_REST_Request $request Request. * @return \WP_REST_Response|WP_Error */ public function rest_enable( WP_REST_Request $request ) { $result = Webhooks::enable( (int) $request['id'] ); if ( is_wp_error( $result ) ) { return $result; } $row = Webhooks::row( $result['webhook'] ); /* translators: %s: webhook name. */ $message = sprintf( __( '“%s” is active again. New events will be delivered.', 'admin-powerups-for-woocommerce' ), $this->plain( $row['name'] ) ); $ping = $result['ping']; $ok = true; if ( $result['pinged'] ) { if ( null === $ping ) { $message .= ' ' . __( 'WooCommerce didn’t send its test ping.', 'admin-powerups-for-woocommerce' ); } elseif ( 200 === (int) $ping['code'] ) { $message .= ' ' . __( 'WooCommerce’s test ping got 200.', 'admin-powerups-for-woocommerce' ); } elseif ( (int) $ping['code'] > 0 ) { $ok = false; /* translators: %d: HTTP status code. */ $message .= ' ' . sprintf( __( 'WooCommerce’s test ping got %d (it only accepts 200).', 'admin-powerups-for-woocommerce' ), (int) $ping['code'] ); } else { $ok = false; /* translators: %s: error message. */ $message .= ' ' . sprintf( __( 'WooCommerce’s test ping failed: %s', 'admin-powerups-for-woocommerce' ), '' !== $ping['error'] ? $ping['error'] : __( 'no answer.', 'admin-powerups-for-woocommerce' ) ); } } return rest_ensure_response( array( 'row_html' => $this->row_html( $row ), 'message' => $message, 'ok' => $ok, 'counts' => Webhooks::counts(), 'pinged' => (bool) $result['pinged'], 'ping' => $ping, ) ); } /** * "5 min ago" for a Unix time. * * @since 1.0.0 * @param int $time Unix time. * @return string */ public function ago( int $time ) { if ( $time <= 0 ) { return ''; } /* translators: %s: time span, e.g. "5 mins". */ return sprintf( __( '%s ago', 'admin-powerups-for-woocommerce' ), human_time_diff( $time, max( $time, time() ) ) ); } /** * Plain text for messages the page shows with textContent. * * @since 1.0.0 * @param mixed $text Text. * @return string */ private function plain( $text ) { return wp_specialchars_decode( Str::scalar( $text ), ENT_QUOTES ); } }