PluginProbe
BeyondWords – AI audio for publishers / trunk
BeyondWords – AI audio for publishers vtrunk
7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.7.0 All 43 releases
speechkit / src / settings / class-utils.php

class-utils.php in BeyondWords – AI audio for publishers trunk, at src/settings/class-utils.php

221 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BeyondWords settings utilities.
4 *
5 * @package BeyondWords\Settings
6 *
7 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
8 */
9
10 declare( strict_types = 1 );
11
12 namespace BeyondWords\Settings;
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Settings utilities.
18 *
19 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
20 */
21 class Utils {
22
23 /**
24 * Built-in post types BeyondWords never considers, regardless of `supports`.
25 *
26 * @var string[]
27 */
28 const SKIP_POST_TYPES = [
29 'attachment',
30 'custom_css',
31 'customize_changeset',
32 'nav_menu_item',
33 'oembed_cache',
34 'revision',
35 'user_request',
36 'wp_block',
37 'wp_font_face',
38 'wp_font_family',
39 'wp_template',
40 'wp_template_part',
41 'wp_global_styles',
42 'wp_navigation',
43 ];
44
45 /**
46 * Required `supports` features for a post type to be eligible.
47 *
48 * BeyondWords needs a title and an editor (body) to generate content,
49 * and custom fields to store post-level audio metadata.
50 *
51 * @var string[]
52 */
53 const REQUIRED_FEATURES = [ 'title', 'editor', 'custom-fields' ];
54
55 /**
56 * How long a connection check is trusted before re-validating.
57 */
58 const CONNECTION_CHECK_TTL = 5 * MINUTE_IN_SECONDS;
59
60 /**
61 * Transient that throttles connection re-validation.
62 *
63 * Holds a fingerprint of the last-checked credentials, so changing the API
64 * key or project ID busts the throttle and forces an immediate re-check.
65 */
66 const CONNECTION_CHECK_TRANSIENT = 'beyondwords_api_connection_checked';
67
68 /**
69 * Get the post types eligible for BeyondWords audio generation.
70 *
71 * @return string[]
72 */
73 public static function get_compatible_post_types(): array {
74 // Reindex before passing to the filter — callers expect a 0-indexed list,
75 // not the associative `[name => name]` shape `get_post_types()` returns.
76 $post_types = array_values( array_diff( get_post_types(), self::SKIP_POST_TYPES ) );
77
78 /**
79 * Filters the post types BeyondWords considers compatible.
80 *
81 * Types lacking the required `supports` features are still dropped.
82 *
83 * @since 3.3.3 Introduced as `beyondwords_post_types`.
84 * @since 4.3.0 Renamed to `beyondwords_settings_post_types`.
85 *
86 * @param string[] $post_types Candidate post type names.
87 */
88 $post_types = apply_filters( 'beyondwords_settings_post_types', $post_types );
89
90 $post_types = array_filter( $post_types, [ self::class, 'post_type_supports_required_features' ] );
91
92 return array_values( $post_types );
93 }
94
95 /**
96 * Whether a post type supports every feature BeyondWords requires.
97 *
98 * Unregistered types pass through — they were added by the
99 * `beyondwords_settings_post_types` filter, which we treat as authoritative.
100 *
101 * @param string $post_type Post type slug.
102 */
103 public static function post_type_supports_required_features( string $post_type ): bool {
104 if ( ! post_type_exists( $post_type ) ) {
105 return true;
106 }
107
108 foreach ( self::REQUIRED_FEATURES as $feature ) {
109 if ( ! post_type_supports( $post_type, $feature ) ) {
110 return false;
111 }
112 }
113
114 return true;
115 }
116
117 /**
118 * Whether both API key and project ID are set.
119 */
120 public static function has_api_creds(): bool {
121 $project_id = trim( (string) get_option( 'beyondwords_project_id' ) );
122 $api_key = trim( (string) get_option( 'beyondwords_api_key' ) );
123
124 return '' !== $project_id && '' !== $api_key;
125 }
126
127 /**
128 * Whether the saved credentials produced a valid REST API connection.
129 *
130 * The flag is set the last time validation succeeded — it does not
131 * prove the credentials are still valid right now.
132 */
133 public static function has_valid_api_connection(): bool {
134 return (bool) get_option( 'beyondwords_valid_api_connection' );
135 }
136
137 /**
138 * Validate the BeyondWords REST API connection and persist the result.
139 *
140 * Throttled per credential fingerprint; only a definitive auth failure
141 * (401/403) clears the stored flag, so an API blip can't hide the other tabs.
142 */
143 public static function validate_api_connection(): bool {
144 $project_id = get_option( 'beyondwords_project_id' );
145 $api_key = get_option( 'beyondwords_api_key' );
146
147 if ( ! $project_id || ! $api_key ) {
148 delete_option( 'beyondwords_valid_api_connection' );
149 return false;
150 }
151
152 $fingerprint = md5( (string) $project_id . '|' . (string) $api_key );
153
154 // Within the throttle window, trust the last result for these credentials.
155 if ( get_transient( self::CONNECTION_CHECK_TRANSIENT ) === $fingerprint ) {
156 return self::has_valid_api_connection();
157 }
158
159 $url = sprintf( '%s/projects/%d', \BeyondWords\Core\Urls::get_api_url(), $project_id );
160 $response = \BeyondWords\Api\Client::call_api( 'GET', $url );
161
162 // Record the attempt whatever the outcome — a down API is throttled too.
163 set_transient( self::CONNECTION_CHECK_TRANSIENT, $fingerprint, self::CONNECTION_CHECK_TTL );
164
165 $response_code = wp_remote_retrieve_response_code( $response );
166
167 if ( 200 === (int) $response_code ) {
168 update_option( 'beyondwords_valid_api_connection', gmdate( \DateTime::ATOM ), false );
169 return true;
170 }
171
172 // 403 is a definitive auth failure (call_api() already handles 401); any
173 // other response is treated as transient and leaves the flag untouched.
174 if ( 403 === (int) $response_code ) {
175 delete_option( 'beyondwords_valid_api_connection' );
176 }
177
178 $debug = sprintf(
179 '<code>%s</code>: <code>%s</code>',
180 $response_code,
181 wp_remote_retrieve_body( $response )
182 );
183
184 self::add_settings_error_message(
185 sprintf(
186 /* translators: %s is replaced with the BeyondWords REST API response debug data */
187 __( 'We were unable to validate your BeyondWords REST API connection.<br />Please check your project ID and API key, save changes, and contact us for support if this message remains.<br /><br />BeyondWords REST API Response:<br />%s', 'speechkit' ),
188 $debug
189 ),
190 'Settings/ValidApiConnection'
191 );
192
193 return false;
194 }
195
196 /**
197 * Queue a settings error message for later rendering.
198 *
199 * Uses a short-lived transient rather than the object cache so the message
200 * survives the Settings API's post-save redirect on any host.
201 *
202 * @param string $message Error message (HTML allowed; rendered through `wp_kses`).
203 * @param string $error_id Optional stable ID; auto-generated when blank.
204 */
205 public static function add_settings_error_message( string $message, string $error_id = '' ): void {
206 $errors = get_transient( 'beyondwords_settings_errors' );
207
208 if ( ! is_array( $errors ) ) {
209 $errors = [];
210 }
211
212 if ( '' === $error_id ) {
213 $error_id = bin2hex( random_bytes( 8 ) );
214 }
215
216 $errors[ $error_id ] = $message;
217
218 set_transient( 'beyondwords_settings_errors', $errors, 30 );
219 }
220 }
221