Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.43% covered (success)
98.43%
125 / 127
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteHealth
98.43% covered (success)
98.43%
125 / 127
81.82% covered (warning)
81.82%
9 / 11
29
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 debug_information
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 add_preferences
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 add_plugin_version
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 add_rest_api_connection
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 get_api_communication
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
3
 resolve_api_host
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 add_filters
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 add_notice_settings
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 add_constant
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 mask_string
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * BeyondWords section for the WordPress Site Health debugging info.
4 *
5 * @package BeyondWords\SiteHealth
6 * @since   3.7.0
7 * @since   7.0.0 Refactored to BeyondWords namespace with snake_case methods.
8 */
9
10declare( strict_types = 1 );
11
12namespace BeyondWords\SiteHealth;
13
14defined( 'ABSPATH' ) || exit;
15
16/**
17 * Site Health debug panel for BeyondWords.
18 *
19 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
20 */
21class SiteHealth {
22
23    /**
24     * BeyondWords filter hooks that are still active.
25     *
26     * Surfaced in Site Health so support can see which extension points are in use.
27     *
28     * @var string[]
29     */
30    const FILTERS = [
31        'beyondwords_content_params',
32        'beyondwords_player_script_onload',
33        'beyondwords_player_html',
34        'beyondwords_player_sdk_params',
35        'beyondwords_settings_player_styles',
36        'beyondwords_settings_post_types',
37        'beyondwords_settings_post_statuses',
38    ];
39
40    /**
41     * BeyondWords filter hooks that are deprecated but may still be in use.
42     *
43     * Used to flag legacy hooks in Site Health so we can chase them down.
44     *
45     * @var string[]
46     */
47    const DEPRECATED_FILTERS = [
48        'beyondwords_amp_player_html',
49        'beyondwords_body_params',
50        'beyondwords_content',
51        'beyondwords_content_id',
52        'beyondwords_js_player_html',
53        'beyondwords_js_player_params',
54        'beyondwords_player_styles',
55        'beyondwords_post_audio_enabled_blocks',
56        'beyondwords_post_metadata',
57        'beyondwords_post_player_enabled',
58        'beyondwords_post_statuses',
59        'beyondwords_post_types',
60        'beyondwords_project_id',
61        'sk_player_after',
62        'sk_player_before',
63        'sk_the_content',
64        'speechkit_amp_player_html',
65        'speechkit_content',
66        'speechkit_js_player_html',
67        'speechkit_js_player_params',
68        'speechkit_post_player_enabled',
69        'speechkit_post_statuses',
70        'speechkit_post_types',
71    ];
72
73    /**
74     * Register WordPress hooks.
75     */
76    public static function init(): void {
77        add_filter( 'debug_information', [ self::class, 'debug_information' ] );
78    }
79
80    /**
81     * Build the BeyondWords section of `debug_information`.
82     *
83     * @param array $info Site Health debug array.
84     *
85     * @return array
86     */
87    public static function debug_information( $info ) {
88        $info['beyondwords']['label'] = __( 'BeyondWords - Text-to-Speech', 'speechkit' );
89
90        self::add_plugin_version( $info );
91        self::add_rest_api_connection( $info );
92
93        $info['beyondwords']['fields']['compatible-post-types'] = [
94            'label' => __( 'Compatible post types', 'speechkit' ),
95            'value' => implode( ', ', \BeyondWords\Settings\Utils::get_compatible_post_types() ),
96        ];
97
98        $info['beyondwords']['fields']['integration-method'] = [
99            'label' => __( 'Integration method', 'speechkit' ),
100            'value' => \BeyondWords\Settings\Fields::get_integration_method(),
101        ];
102
103        $info['beyondwords']['fields']['beyondwords_api_key'] = [
104            'label' => __( 'API Key', 'speechkit' ),
105            'value' => self::mask_string( get_option( 'beyondwords_api_key' ) ),
106        ];
107
108        $info['beyondwords']['fields']['beyondwords_project_id'] = [
109            'label' => __( 'Project ID', 'speechkit' ),
110            'value' => get_option( 'beyondwords_project_id' ),
111        ];
112
113        self::add_preferences( $info );
114        self::add_filters( $info );
115        self::add_notice_settings( $info );
116
117        self::add_constant( $info, 'BEYONDWORDS_AUTOREGENERATE' );
118
119        return $info;
120    }
121
122    /**
123     * Add the per-site preferences (Preferences tab) to the debug array.
124     *
125     * @param array $info Debug array, modified in place.
126     */
127    public static function add_preferences( array &$info ): void {
128        $info['beyondwords']['fields']['beyondwords_prepend_excerpt'] = [
129            'label' => __( 'Include excerpt', 'speechkit' ),
130            'value' => get_option( 'beyondwords_prepend_excerpt' ) ? __( 'Yes', 'speechkit' ) : __( 'No', 'speechkit' ),
131            'debug' => get_option( 'beyondwords_prepend_excerpt' ) ? 'yes' : 'no',
132        ];
133
134        $info['beyondwords']['fields']['beyondwords_player_ui'] = [
135            'label' => __( 'Player UI', 'speechkit' ),
136            'value' => get_option( 'beyondwords_player_ui' ),
137        ];
138
139        $info['beyondwords']['fields']['beyondwords_preselect'] = [
140            'label' => __( 'Preselect â€˜Generate audio’', 'speechkit' ),
141            'value' => (string) wp_json_encode( get_option( 'beyondwords_preselect' ), JSON_PRETTY_PRINT ),
142        ];
143    }
144
145    /**
146     * Add plugin version (with file/db version mismatch detection) to the debug array.
147     *
148     * @param array $info Debug array, modified in place.
149     */
150    public static function add_plugin_version( array &$info ): void {
151        $const_version = defined( 'BEYONDWORDS__PLUGIN_VERSION' ) ? BEYONDWORDS__PLUGIN_VERSION : '';
152        $db_version    = get_option( 'beyondwords_version' );
153
154        if ( $const_version && $const_version === $db_version ) {
155            $info['beyondwords']['fields']['plugin-version'] = [
156                'label' => __( 'Plugin version', 'speechkit' ),
157                'value' => BEYONDWORDS__PLUGIN_VERSION,
158            ];
159            return;
160        }
161
162        $info['beyondwords']['fields']['plugin-version'] = [
163            'label' => __( 'Plugin version', 'speechkit' ),
164            'value' => sprintf(
165                /* translators: 1: Current plugin version, 2: Database plugin version */
166                __( 'Version mismatch: file: %1$s / db: %2$s', 'speechkit' ),
167                $const_version,
168                $db_version
169            ),
170        ];
171    }
172
173    /**
174     * Add REST API connectivity info to the debug array.
175     *
176     * @param array $info Debug array, modified in place.
177     */
178    public static function add_rest_api_connection( array &$info ): void {
179        $api_url = \BeyondWords\Core\Urls::get_api_url();
180
181        $info['beyondwords']['fields']['api-url'] = [
182            'label' => __( 'REST API URL', 'speechkit' ),
183            'value' => $api_url,
184        ];
185
186        $info['beyondwords']['fields']['api-communication'] = array_merge(
187            [ 'label' => __( 'Communication with REST API', 'speechkit' ) ],
188            self::get_api_communication( $api_url )
189        );
190    }
191
192    /**
193     * Resolve the "Communication with REST API" value/debug pair.
194     *
195     * Deliberately not cached: Site Health is a diagnostic, and core's own
196     * `dotorg_communication` field likewise probes uncached on every render.
197     *
198     * @param string $api_url BeyondWords REST API base URL.
199     *
200     * @return array<string,string>
201     */
202    private static function get_api_communication( string $api_url ): array {
203        // Nothing to probe until the site has API credentials.
204        if ( ! \BeyondWords\Settings\Utils::has_api_creds() ) {
205            return [
206                'value' => __( 'Not checked â€” BeyondWords API credentials are not configured', 'speechkit' ),
207                'debug' => 'not-configured',
208            ];
209        }
210
211        // A plain request on purpose: `call_api()` would clear the valid-connection
212        // flag on a 401, and the VIP helper's circuit breaker can fake failures.
213        $response = wp_remote_request(
214            $api_url,
215            [
216                'method'  => 'GET',
217                'timeout' => \BeyondWords\Api\Client::DEFAULT_REQUEST_TIMEOUT,
218            ]
219        );
220
221        if ( ! is_wp_error( $response ) ) {
222            return [
223                'value' => __( 'BeyondWords API is reachable', 'speechkit' ),
224                'debug' => 'true',
225            ];
226        }
227
228        return [
229            'value' => sprintf(
230                /* translators: 1: The IP address the REST API resolves to. 2: The error returned by the request. */
231                __( 'Unable to reach BeyondWords API at %1$s: %2$s', 'speechkit' ),
232                self::resolve_api_host( $api_url ),
233                $response->get_error_message()
234            ),
235            'debug' => $response->get_error_message(),
236        ];
237    }
238
239    /**
240     * Resolve the API host to an IP address for the connectivity diagnostic.
241     *
242     * `gethostbyname()` needs a bare hostname, so the URL is parsed first; it
243     * returns the host (or URL) unchanged when resolution fails.
244     *
245     * @param string $api_url BeyondWords REST API base URL.
246     */
247    private static function resolve_api_host( string $api_url ): string {
248        $host = wp_parse_url( $api_url, PHP_URL_HOST );
249
250        if ( ! is_string( $host ) || '' === $host ) {
251            return $api_url;
252        }
253
254        return gethostbyname( $host );
255    }
256
257    /**
258     * Add registered + deprecated filter hook info to the debug array.
259     *
260     * @param array $info Debug array, modified in place.
261     */
262    public static function add_filters( array &$info ): void {
263        $registered = array_values( array_filter( self::FILTERS, 'has_filter' ) );
264
265        $info['beyondwords']['fields']['registered-filters'] = [
266            'label' => __( 'Registered filters', 'speechkit' ),
267            'value' => empty( $registered ) ? __( 'None', 'speechkit' ) : implode( ', ', $registered ),
268            'debug' => empty( $registered ) ? 'none' : implode( ', ', $registered ),
269        ];
270
271        $registered = array_values( array_filter( self::DEPRECATED_FILTERS, 'has_filter' ) );
272
273        $info['beyondwords']['fields']['registered-deprecated-filters'] = [
274            'label' => __( 'Registered deprecated filters', 'speechkit' ),
275            'value' => empty( $registered ) ? __( 'None', 'speechkit' ) : implode( ', ', $registered ),
276            'debug' => empty( $registered ) ? 'none' : implode( ', ', $registered ),
277        ];
278    }
279
280    /**
281     * Add notice/activation timestamps to the debug array.
282     *
283     * @param array $info Debug array, modified in place.
284     */
285    public static function add_notice_settings( array &$info ): void {
286        $info['beyondwords']['fields']['beyondwords_date_activated'] = [
287            'label' => __( 'Date Activated', 'speechkit' ),
288            'value' => get_option( 'beyondwords_date_activated', '' ),
289        ];
290
291        $info['beyondwords']['fields']['beyondwords_notice_review_dismissed'] = [
292            'label' => __( 'Review Notice Dismissed', 'speechkit' ),
293            'value' => get_option( 'beyondwords_notice_review_dismissed', '' ),
294        ];
295    }
296
297    /**
298     * Add a single constant's value to the debug array.
299     *
300     * @param array  $info Debug array, modified in place.
301     * @param string $name Constant name.
302     */
303    public static function add_constant( array &$info, string $name ): void {
304        $value = __( 'Undefined', 'speechkit' );
305
306        if ( defined( $name ) ) {
307            $value = constant( $name );
308
309            if ( is_bool( $value ) ) {
310                $value = true === $value ? 'True' : 'False';
311            }
312        }
313
314        $info['beyondwords']['fields'][ $name ] = [
315            'label' => $name,
316            'value' => $value,
317            'debug' => $value,
318        ];
319    }
320
321    /**
322     * Mask a sensitive string for display in Site Health.
323     *
324     * @param string|false $value Value to mask. `false` (e.g. missing option) renders empty.
325     * @param int          $count Number of trailing characters to preserve.
326     * @param string       $char  Character used to mask.
327     */
328    public static function mask_string( string|false $value, int $count = 4, string $char = 'X' ): string {
329        if ( ! is_string( $value ) ) {
330            return '';
331        }
332
333        if ( strlen( $value ) < 8 ) {
334            return str_repeat( $char, strlen( $value ) );
335        }
336
337        return str_repeat( $char, strlen( $value ) - $count ) . substr( $value, -$count );
338    }
339}