id, array( 'dashboard', 'plugins' ), true ) ) { return; } // The notice only invites setup, so it goes away for good once an endpoint exists. if ( Endpoints::all() ) { delete_option( self::WELCOME_OPTION ); return; } $start = admin_url( 'admin.php?page=hookbird&hookbird_tour=1' ); $dismiss = wp_nonce_url( admin_url( 'admin-post.php?action=hookbird_dismiss_welcome' ), 'hookbird_dismiss_welcome' ); ?>
403 ) ); } check_admin_referer( 'hookbird_dismiss_welcome' ); delete_option( self::WELCOME_OPTION ); wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'plugins.php' ) ); exit; } /** * Remember that the tour was finished or skipped. */ public function ajax_tour_done() { check_ajax_referer( 'hookbird_tour', 'nonce' ); if ( ! current_user_can( Admin::CAP ) ) { wp_send_json_error( null, 403 ); } update_user_meta( get_current_user_id(), self::TOUR_META, time() ); delete_option( self::WELCOME_OPTION ); wp_send_json_success(); } /** * Setup checklist state for the endpoints screen. * * @return array[] Each item: key, label, help, done, url. */ public static function checklist() { $endpoints = Endpoints::all(); $has_ep = ! empty( $endpoints ); $tested = false; $real = false; if ( $has_ep ) { global $wpdb; $table = Installer::deliveries_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Custom table, two cheap existence checks on an admin screen. $tested = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM %i WHERE is_test = 1 AND status = 'success' LIMIT 1", $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- See above. $real = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM %i WHERE is_test = 0 AND status = 'success' LIMIT 1", $table ) ); } $first = $has_ep ? admin_url( 'admin.php?page=hookbird-edit&id=' . $endpoints[0]['id'] ) : admin_url( 'admin.php?page=hookbird-edit' ); return array( array( 'key' => 'endpoint', 'label' => __( 'Connect a destination', 'hookbird' ), 'help' => __( 'Pick Slack, Discord, n8n or any URL and choose the events it should get.', 'hookbird' ), 'done' => $has_ep, 'url' => admin_url( 'admin.php?page=hookbird-edit' ), ), array( 'key' => 'test', 'label' => __( 'Send a test event', 'hookbird' ), 'help' => __( 'Sample data shows you exactly what will arrive, before anything real happens.', 'hookbird' ), 'done' => $tested, 'url' => $first, ), array( 'key' => 'live', 'label' => __( 'Receive your first real event', 'hookbird' ), 'help' => __( 'Publish a post, register a user or place an order and watch it arrive.', 'hookbird' ), 'done' => $real, 'url' => admin_url( 'admin.php?page=hookbird-log' ), ), ); } /** * Health checks for the Help & Setup screen. * * @return array[] Each: label, status (ok|warn|fail|info), detail. */ public static function status_checks() { global $wpdb, $wp_version; $checks = array(); $checks[] = array( 'label' => __( 'PHP version', 'hookbird' ), 'status' => version_compare( PHP_VERSION, '7.4', '>=' ) ? 'ok' : 'fail', 'detail' => PHP_VERSION, ); $checks[] = array( 'label' => __( 'WordPress version', 'hookbird' ), 'status' => version_compare( $wp_version, '6.2', '>=' ) ? 'ok' : 'fail', 'detail' => $wp_version, ); $tables = 0; foreach ( array( Installer::endpoints_table(), Installer::deliveries_table() ) as $table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Diagnostic check on an admin screen. if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) { ++$tables; } } $checks[] = array( 'label' => __( 'Database tables', 'hookbird' ), 'status' => 2 === $tables ? 'ok' : 'fail', 'detail' => 2 === $tables ? __( 'Both tables are in place.', 'hookbird' ) : __( 'Missing. Deactivate and activate Hookbird to create them.', 'hookbird' ), ); $https = function_exists( 'curl_version' ) || extension_loaded( 'openssl' ); $checks[] = array( 'label' => __( 'Secure connections', 'hookbird' ), 'status' => $https ? 'ok' : 'fail', 'detail' => $https ? __( 'cURL or OpenSSL is available for HTTPS requests.', 'hookbird' ) : __( 'Neither cURL nor OpenSSL is available, so HTTPS webhooks cannot be sent. Ask your host to enable one.', 'hookbird' ), ); $blocked = defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL; $checks[] = array( 'label' => __( 'Outgoing requests', 'hookbird' ), 'status' => $blocked ? 'warn' : 'ok', 'detail' => $blocked ? __( 'WP_HTTP_BLOCK_EXTERNAL is on. Add your webhook hosts to WP_ACCESSIBLE_HOSTS in wp-config.php or deliveries will fail.', 'hookbird' ) : __( 'WordPress is allowed to contact external services.', 'hookbird' ), ); $cron_off = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON; $checks[] = array( 'label' => __( 'Background processing', 'hookbird' ), 'status' => $cron_off ? 'warn' : 'ok', 'detail' => $cron_off ? __( 'WP-Cron is disabled. Make sure a server cron job calls wp-cron.php every minute, or runs "wp hookbird run".', 'hookbird' ) : __( 'WP-Cron is enabled. Deliveries are sent in the background right after each page load.', 'hookbird' ), ); $last = (int) get_option( self::LAST_RUN ); $checks[] = array( 'label' => __( 'Last queue run', 'hookbird' ), 'status' => $last ? 'ok' : 'info', /* translators: %s: human time difference. */ 'detail' => $last ? sprintf( __( '%s ago', 'hookbird' ), human_time_diff( $last ) ) : __( 'Not yet. It runs after the first event is queued.', 'hookbird' ), ); $next = Deliveries::next_due_time(); $overdue = $next && $next < time() - 5 * MINUTE_IN_SECONDS; $pending = Deliveries::count( 'pending' ); $checks[] = array( 'label' => __( 'Queue', 'hookbird' ), 'status' => $overdue ? 'warn' : 'ok', 'detail' => $overdue /* translators: %d: number of deliveries. */ ? sprintf( _n( '%d delivery is overdue. Background processing may not be running.', '%d deliveries are waiting and some are overdue. Background processing may not be running.', $pending, 'hookbird' ), $pending ) /* translators: %d: number of deliveries. */ : sprintf( _n( '%d delivery waiting.', '%d deliveries waiting.', $pending, 'hookbird' ), $pending ), ); $paused = 0; foreach ( Endpoints::all() as $endpoint ) { if ( $endpoint['paused_at'] ) { ++$paused; } } $checks[] = array( 'label' => __( 'Paused endpoints', 'hookbird' ), 'status' => $paused ? 'warn' : 'ok', 'detail' => $paused /* translators: %d: number of endpoints. */ ? sprintf( _n( '%d endpoint was paused after repeated failures. Fix it and click Resume.', '%d endpoints were paused after repeated failures. Fix them and click Resume.', $paused, 'hookbird' ), $paused ) : __( 'None.', 'hookbird' ), ); $checks[] = array( 'label' => __( 'Local and private URLs', 'hookbird' ), 'status' => 'info', 'detail' => Settings::allow_local_urls() ? __( 'Allowed. Endpoints can point at localhost, your network and custom ports.', 'hookbird' ) : __( 'Blocked for safety. Turn this on in Settings to use a self-hosted n8n on your network or a custom port.', 'hookbird' ), ); $checks[] = array( 'label' => __( 'WooCommerce', 'hookbird' ), 'status' => 'info', 'detail' => class_exists( 'WooCommerce' ) ? __( 'Active. Order and stock events are available.', 'hookbird' ) : __( 'Not active. Order and stock events start working once it is.', 'hookbird' ), ); return $checks; } /** * Check that WordPress can reach itself, which WP-Cron needs to run in the background. */ public function handle_check_loopback() { if ( ! current_user_can( Admin::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'hookbird' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'hookbird_check_loopback' ); $response = wp_remote_post( site_url( 'wp-cron.php' ), array( 'timeout' => 10, 'blocking' => true, // The loopback goes to this same site, which may use a self-signed certificate locally. 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core filter. ) ); $key = 'hookbird_notices_' . get_current_user_id(); $notices = (array) get_transient( $key ); if ( is_wp_error( $response ) ) { $notices[] = array( 'type' => 'error', /* translators: %s: error message. */ 'message' => sprintf( __( 'Your site could not reach itself: %s Background deliveries need this. Ask your host about loopback requests, or set up a server cron job.', 'hookbird' ), $response->get_error_message() ), ); } else { $code = (int) wp_remote_retrieve_response_code( $response ); $notices[] = $code >= 200 && $code < 400 ? array( 'type' => 'success', /* translators: %d: HTTP status code. */ 'message' => sprintf( __( 'Background processing works. Your site reached wp-cron.php (HTTP %d).', 'hookbird' ), $code ), ) : array( 'type' => 'warning', /* translators: %d: HTTP status code. */ 'message' => sprintf( __( 'Your site reached wp-cron.php but got HTTP %d. A security plugin or server rule may be blocking it.', 'hookbird' ), $code ), ); } set_transient( $key, $notices, 5 * MINUTE_IN_SECONDS ); wp_safe_redirect( admin_url( 'admin.php?page=hookbird-help#status' ) ); exit; } /** * Step-by-step setup guides per destination. * * @return array key => [ label, kind, steps[] ] */ public static function guides() { return array( 'slack' => array( 'label' => 'Slack', 'kind' => __( 'Team chat', 'hookbird' ), 'steps' => array( __( 'In Slack, create a channel for the alerts, for example #shop-alerts.', 'hookbird' ), __( 'Open api.slack.com/apps, click Create New App, choose From scratch, name it and pick your workspace.', 'hookbird' ), __( 'Open Incoming Webhooks, switch it on, click Add New Webhook, choose the channel and click Allow.', 'hookbird' ), __( 'Copy the URL that starts with https://hooks.slack.com/services/.', 'hookbird' ), __( 'In Hookbird, add an endpoint, choose Slack, paste the URL, pick events and send a test.', 'hookbird' ), ), ), 'discord' => array( 'label' => 'Discord', 'kind' => __( 'Team chat', 'hookbird' ), 'steps' => array( __( 'Hover the channel in your server and click the gear icon (Edit Channel).', 'hookbird' ), __( 'Open Integrations, then Webhooks, then New Webhook. Give it a name such as Hookbird.', 'hookbird' ), __( 'Click Copy Webhook URL.', 'hookbird' ), __( 'In Hookbird, add an endpoint, choose Discord, paste the URL, pick events and send a test.', 'hookbird' ), ), ), 'telegram' => array( 'label' => 'Telegram', 'kind' => __( 'Messaging', 'hookbird' ), 'steps' => array( __( 'Message @BotFather, send /newbot and follow the prompts. Copy the bot token it gives you.', 'hookbird' ), __( 'Add the bot to your group or channel. For a channel, make it an admin so it can post.', 'hookbird' ), __( 'Find the chat ID: a public channel can use @channelname; for a group, send a message and open api.telegram.org/bot