PluginProbe
BeyondWords – AI audio for publishers / 7.0.0
BeyondWords – AI audio for publishers v7.0.0
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-settings.php

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

433 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BeyondWords settings page: admin menu, tabbed form, REST endpoints, notices.
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 page.
18 *
19 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
20 */
21 class Settings {
22
23 const PAGE_SLUG = 'beyondwords';
24 const REVIEW_NOTICE_OFFSET = '-14 days';
25
26 /**
27 * Register WordPress hooks.
28 */
29 public static function init(): void {
30 add_action( 'admin_menu', [ self::class, 'add_options_page' ], 1 );
31 add_action( 'admin_notices', [ self::class, 'maybe_print_missing_creds_warning' ], 100 );
32 add_action( 'admin_notices', [ self::class, 'print_settings_errors' ], 200 );
33 add_action( 'admin_notices', [ self::class, 'maybe_print_review_notice' ] );
34 add_action( 'load-settings_page_' . self::PAGE_SLUG, [ self::class, 'maybe_validate_api_creds' ] );
35
36 add_action( 'rest_api_init', [ self::class, 'register_rest_routes' ] );
37
38 add_filter( 'plugin_action_links_speechkit/speechkit.php', [ self::class, 'add_plugin_action_link' ] );
39 }
40
41 /**
42 * Register the BeyondWords settings page under Settings.
43 */
44 public static function add_options_page(): void {
45 add_options_page(
46 __( 'BeyondWords Settings', 'speechkit' ),
47 __( 'BeyondWords', 'speechkit' ),
48 'manage_options',
49 self::PAGE_SLUG,
50 [ self::class, 'render_admin_page' ]
51 );
52 }
53
54 /**
55 * Validate API credentials whenever the Authentication tab loads.
56 *
57 * Triggered on the page-specific load hook so we don't pay the API cost
58 * on unrelated admin screens.
59 */
60 public static function maybe_validate_api_creds(): void {
61 if ( Tabs::TAB_AUTHENTICATION === Tabs::get_active_tab() ) {
62 Utils::validate_api_connection();
63 }
64 }
65
66 /**
67 * Render the settings page (the tabbed form).
68 */
69 public static function render_admin_page(): void {
70 $tabs = Tabs::get_visible_tabs();
71 $active_tab = Tabs::get_active_tab();
72 $active = Tabs::get_active_page_and_group();
73 ?>
74 <div class="wrap">
75 <h1><?php esc_html_e( 'BeyondWords Settings', 'speechkit' ); ?></h1>
76
77 <form
78 id="beyondwords-plugin-settings"
79 action="<?php echo esc_url( admin_url( 'options.php' ) ); ?>"
80 method="post"
81 >
82 <nav class="nav-tab-wrapper">
83 <ul>
84 <?php foreach ( $tabs as $slug => $label ) : ?>
85 <li>
86 <a
87 class="nav-tab <?php echo $slug === $active_tab ? 'nav-tab-active' : ''; ?>"
88 href="<?php echo esc_url( add_query_arg( [ 'page' => self::PAGE_SLUG, 'tab' => $slug ] ) ); ?>"
89 >
90 <?php echo esc_html( $label ); ?>
91 </a>
92 </li>
93 <?php endforeach; ?>
94 </ul>
95 </nav>
96
97 <hr class="wp-header-end">
98
99 <?php
100 settings_fields( $active['group'] );
101 do_settings_sections( $active['page'] );
102 submit_button( __( 'Save changes', 'speechkit' ) );
103 ?>
104 </form>
105 </div>
106 <?php
107 }
108
109 /**
110 * Add a "Settings" link to the plugin row.
111 *
112 * @param string[] $links Existing action links.
113 * @return string[]
114 */
115 public static function add_plugin_action_link( array $links ): array {
116 $settings_link = sprintf(
117 '<a href="%s">%s</a>',
118 esc_url( admin_url( 'options-general.php?page=' . self::PAGE_SLUG ) ),
119 esc_html__( 'Settings', 'speechkit' )
120 );
121
122 array_unshift( $links, $settings_link );
123 return $links;
124 }
125
126 /**
127 * Show a banner pointing to the settings page until creds are entered.
128 */
129 public static function maybe_print_missing_creds_warning(): void {
130 if ( Utils::has_api_creds() ) {
131 return;
132 }
133 ?>
134 <div class="notice notice-info">
135 <p>
136 <strong>
137 <?php
138 printf(
139 /* translators: %s is the "plugin settings" link */
140 esc_html__( 'To use BeyondWords, please update the %s.', 'speechkit' ),
141 sprintf(
142 '<a href="%s">%s</a>',
143 esc_url( admin_url( 'options-general.php?page=' . self::PAGE_SLUG ) ),
144 esc_html__( 'plugin settings', 'speechkit' )
145 )
146 );
147 ?>
148 </strong>
149 </p>
150 <p><?php esc_html_e( 'Don’t have a BeyondWords account yet?', 'speechkit' ); ?></p>
151 <p>
152 <a
153 class="button button-secondary"
154 href="<?php echo esc_url( sprintf( '%s/auth/signup', \BeyondWords\Core\Urls::get_dashboard_url() ) ); ?>"
155 target="_blank"
156 >
157 <?php esc_html_e( 'Sign up free', 'speechkit' ); ?>
158 </a>
159 </p>
160 </div>
161 <?php
162 }
163
164 /**
165 * Show the once-only review notice 14+ days after activation.
166 */
167 public static function maybe_print_review_notice(): void {
168 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
169 if ( ! $screen || 'settings_page_' . self::PAGE_SLUG !== $screen->id ) {
170 return;
171 }
172
173 $dismissed = get_option( 'beyondwords_notice_review_dismissed', '' );
174 if ( ! empty( $dismissed ) ) {
175 return;
176 }
177
178 $activated_at = strtotime( (string) get_option( 'beyondwords_date_activated', '2025-03-01' ) );
179 if ( false === $activated_at || $activated_at >= strtotime( self::REVIEW_NOTICE_OFFSET ) ) {
180 return;
181 }
182 ?>
183 <div id="beyondwords_notice_review" class="notice notice-info is-dismissible">
184 <p>
185 <strong>
186 <?php
187 printf(
188 /* translators: %s is the link to the WordPress plugin repo */
189 esc_html__( 'Happy with our work? Help us spread the word with a rating on the %s.', 'speechkit' ),
190 sprintf(
191 '<a href="%s">%s</a>',
192 'https://wordpress.org/support/plugin/speechkit/reviews/',
193 esc_html__( 'WordPress Plugin Repo', 'speechkit' )
194 )
195 );
196 ?>
197 </strong>
198 </p>
199 </div>
200 <?php
201 }
202
203 /**
204 * Drain queued settings errors into a notice.
205 *
206 * `admin_notices` fires on every admin screen, so this early-returns off the
207 * settings page — the only screen the errors are ever queued for.
208 */
209 public static function print_settings_errors(): void {
210 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
211 if ( ! $screen || 'settings_page_' . self::PAGE_SLUG !== $screen->id ) {
212 return;
213 }
214
215 $errors = get_transient( 'beyondwords_settings_errors' );
216
217 if ( ! is_array( $errors ) || empty( $errors ) ) {
218 return;
219 }
220
221 delete_transient( 'beyondwords_settings_errors' );
222
223 $allowed = [
224 'a' => [ 'href' => [], 'target' => [] ],
225 'b' => [],
226 'strong' => [],
227 'i' => [],
228 'em' => [],
229 'br' => [],
230 'code' => [],
231 ];
232 ?>
233 <div class="notice notice-error">
234 <ul class="ul-disc">
235 <?php foreach ( $errors as $error ) : ?>
236 <li><?php echo wp_kses( $error, $allowed ); ?></li>
237 <?php endforeach; ?>
238 </ul>
239 </div>
240 <?php
241 }
242
243 /**
244 * Register REST routes consumed by the editor scripts.
245 */
246 public static function register_rest_routes(): void {
247 register_rest_route(
248 'beyondwords/v1',
249 '/settings',
250 [
251 'methods' => \WP_REST_Server::READABLE,
252 'callback' => [ self::class, 'rest_settings_response' ],
253 'permission_callback' => static fn() => current_user_can( 'edit_posts' ),
254 ]
255 );
256
257 register_rest_route(
258 'beyondwords/v1',
259 '/settings/notices/review/dismiss',
260 [
261 'methods' => \WP_REST_Server::CREATABLE,
262 'callback' => [ self::class, 'rest_dismiss_review_notice' ],
263 'permission_callback' => static fn() => current_user_can( 'manage_options' ),
264 ]
265 );
266
267 register_rest_route(
268 'beyondwords/v1',
269 '/projects/(?P<projectId>[0-9]+)/video-settings',
270 [
271 'methods' => \WP_REST_Server::READABLE,
272 'callback' => [ self::class, 'rest_video_settings_response' ],
273 'permission_callback' => static fn() => current_user_can( 'edit_posts' ),
274 ]
275 );
276
277 register_rest_route(
278 'beyondwords/v1',
279 '/projects/(?P<projectId>[0-9]+)',
280 [
281 'methods' => \WP_REST_Server::READABLE,
282 'callback' => [ self::class, 'rest_project_response' ],
283 'permission_callback' => static fn() => current_user_can( 'edit_posts' ),
284 ]
285 );
286
287 register_rest_route(
288 'beyondwords/v1',
289 '/summarization-settings-templates',
290 [
291 'methods' => \WP_REST_Server::READABLE,
292 'callback' => [ self::class, 'rest_summarization_settings_templates_response' ],
293 'permission_callback' => static fn() => current_user_can( 'edit_posts' ),
294 ]
295 );
296
297 register_rest_route(
298 'beyondwords/v1',
299 '/video-settings-templates',
300 [
301 'methods' => \WP_REST_Server::READABLE,
302 'callback' => [ self::class, 'rest_video_settings_templates_response' ],
303 'permission_callback' => static fn() => current_user_can( 'edit_posts' ),
304 ]
305 );
306 }
307
308 /**
309 * Settings payload for editor scripts.
310 *
311 * Never include the API key in this response — editor scripts run in the
312 * browser and the key must stay server-side.
313 */
314 public static function rest_settings_response(): \WP_REST_Response {
315 global $wp_version;
316
317 return new \WP_REST_Response(
318 [
319 'inspectMetaKeys' => self::inspect_meta_keys(),
320 'integrationMethod' => Fields::get_integration_method(),
321 'pluginVersion' => BEYONDWORDS__PLUGIN_VERSION,
322 'projectId' => (string) get_option( Fields::OPTION_PROJECT_ID, '' ),
323 'preselect' => Preselect::get(),
324 'restUrl' => get_rest_url(),
325 'wpVersion' => $wp_version,
326 ]
327 );
328 }
329
330 /**
331 * Meta keys surfaced in the editor Inspect panel.
332 *
333 * Sourced from the canonical lists in \BeyondWords\Core\Utils, minus
334 * internal-only keys that have never been shown in the panel.
335 *
336 * @since 7.0.0
337 *
338 * @return array{current: string[], deprecated: string[]}
339 */
340 private static function inspect_meta_keys(): array {
341 $internal_only = [
342 'beyondwords_player_content',
343 'beyondwords_player_style',
344 'beyondwords_title_voice_id',
345 'beyondwords_summary_voice_id',
346 'beyondwords_disabled',
347 'beyondwords_hash',
348 'speechkit_hash',
349 'speechkit_updated_at',
350 ];
351
352 return [
353 'current' => \BeyondWords\Core\Utils::get_post_meta_keys( 'current' ),
354 'deprecated' => array_values(
355 array_diff(
356 \BeyondWords\Core\Utils::get_post_meta_keys( 'deprecated' ),
357 $internal_only
358 )
359 ),
360 ];
361 }
362
363 /**
364 * Mark the review notice as dismissed.
365 */
366 public static function rest_dismiss_review_notice(): \WP_REST_Response {
367 $saved = update_option( 'beyondwords_notice_review_dismissed', gmdate( \DateTime::ATOM ) );
368
369 return new \WP_REST_Response(
370 [ 'success' => $saved ],
371 $saved ? 200 : 500
372 );
373 }
374
375 /**
376 * Proxy for the BeyondWords video settings endpoint.
377 *
378 * Editor scripts read the `sizes` array to populate the "Video size" dropdown.
379 *
380 * @since 7.0.0
381 *
382 * @param \WP_REST_Request $request The REST request.
383 */
384 public static function rest_video_settings_response( \WP_REST_Request $request ): \WP_REST_Response {
385 $project_id = (int) $request->get_param( 'projectId' );
386 $response = \BeyondWords\Api\Client::get_video_settings( $project_id );
387
388 return new \WP_REST_Response( $response );
389 }
390
391 /**
392 * Proxy for the BeyondWords project endpoint.
393 *
394 * Editor scripts read the project's default `language` for the Language dropdown.
395 *
396 * @since 7.0.0
397 *
398 * @param \WP_REST_Request $request The REST request.
399 */
400 public static function rest_project_response( \WP_REST_Request $request ): \WP_REST_Response {
401 $project_id = (int) $request->get_param( 'projectId' );
402 $response = \BeyondWords\Api\Client::get_project( $project_id );
403
404 return new \WP_REST_Response( $response );
405 }
406
407 /**
408 * Proxy for the BeyondWords summarization settings templates endpoint.
409 *
410 * Editor scripts use this list to populate the "Script template" dropdown.
411 *
412 * @since 7.0.0
413 */
414 public static function rest_summarization_settings_templates_response(): \WP_REST_Response {
415 $response = \BeyondWords\Api\Client::get_summarization_settings_templates();
416
417 return new \WP_REST_Response( $response );
418 }
419
420 /**
421 * Proxy for the BeyondWords video settings templates endpoint.
422 *
423 * Editor scripts use this list to populate the "Video template" dropdown.
424 *
425 * @since 7.0.0
426 */
427 public static function rest_video_settings_templates_response(): \WP_REST_Response {
428 $response = \BeyondWords\Api\Client::get_video_settings_templates();
429
430 return new \WP_REST_Response( $response );
431 }
432 }
433