Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.10% |
184 / 245 |
|
50.00% |
7 / 14 |
CRAP | |
0.00% |
0 / 1 |
Settings | |
75.10% |
184 / 245 |
|
50.00% |
7 / 14 |
53.91 | |
0.00% |
0 / 1 |
init | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
addOptionsPage | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
maybeValidateApiCreds | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
createAdminInterface | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
4 | |||
addSettingsLinkToPluginPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getTabs | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
getActiveTab | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
5.51 | |||
printMissingApiCredsWarning | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
2 | |||
maybePrintPluginReviewNotice | |
96.30% |
26 / 27 |
|
0.00% |
0 / 1 |
6 | |||
printSettingsErrors | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
4 | |||
restApiInit | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
1.00 | |||
restApiResponse | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
dismissReviewNotice | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
enqueueScripts | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | /** |
6 | * BeyondWords settings. |
7 | * |
8 | * @package Beyondwords\Wordpress |
9 | * @author Stuart McAlpine <stu@beyondwords.io> |
10 | * @since 3.0.0 |
11 | */ |
12 | |
13 | namespace Beyondwords\Wordpress\Component\Settings; |
14 | |
15 | use Beyondwords\Wordpress\Component\Settings\Fields\Languages\Languages; |
16 | use Beyondwords\Wordpress\Component\Settings\Fields\PreselectGenerateAudio\PreselectGenerateAudio; |
17 | use Beyondwords\Wordpress\Component\Settings\Tabs\Content\Content; |
18 | use Beyondwords\Wordpress\Component\Settings\Tabs\Credentials\Credentials; |
19 | use Beyondwords\Wordpress\Component\Settings\Tabs\Player\Player; |
20 | use Beyondwords\Wordpress\Component\Settings\Tabs\Pronunciations\Pronunciations; |
21 | use Beyondwords\Wordpress\Component\Settings\Tabs\Summarization\Summarization; |
22 | use Beyondwords\Wordpress\Component\Settings\Tabs\Voices\Voices; |
23 | use Beyondwords\Wordpress\Component\Settings\SettingsUtils; |
24 | use Beyondwords\Wordpress\Component\Settings\Sync; |
25 | use Beyondwords\Wordpress\Core\Environment; |
26 | |
27 | /** |
28 | * Settings |
29 | * |
30 | * @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
31 | * |
32 | * @since 3.0.0 |
33 | */ |
34 | class Settings |
35 | { |
36 | public const REVIEW_NOTICE_TIME_FORMAT = '-14 days'; |
37 | |
38 | /** |
39 | * Init |
40 | * |
41 | * @since 3.0.0 Introduced. |
42 | * @since 5.4.0 Add plugin review notice. |
43 | */ |
44 | public function init() |
45 | { |
46 | (new Credentials())->init(); |
47 | (new Sync())->init(); |
48 | |
49 | if (SettingsUtils::hasValidApiConnection()) { |
50 | (new Voices())->init(); |
51 | (new Content())->init(); |
52 | (new Player())->init(); |
53 | (new Summarization())->init(); |
54 | (new Pronunciations())->init(); |
55 | } |
56 | |
57 | add_action('admin_menu', array($this, 'addOptionsPage'), 1); |
58 | add_action('admin_notices', array($this, 'printMissingApiCredsWarning'), 100); |
59 | add_action('admin_notices', array($this, 'printSettingsErrors'), 200); |
60 | add_action('admin_notices', array($this, 'maybePrintPluginReviewNotice')); |
61 | add_action('admin_enqueue_scripts', array($this, 'enqueueScripts')); |
62 | add_action('load-settings_page_beyondwords', array($this, 'maybeValidateApiCreds')); |
63 | |
64 | add_action('rest_api_init', array($this, 'restApiInit')); |
65 | |
66 | add_filter('plugin_action_links_speechkit/speechkit.php', array($this, 'addSettingsLinkToPluginPage')); |
67 | } |
68 | |
69 | /** |
70 | * Add items to the WordPress admin menu. |
71 | * |
72 | * @since 3.0.0 |
73 | * |
74 | * @return void |
75 | */ |
76 | public function addOptionsPage() |
77 | { |
78 | // Settings > BeyondWords |
79 | add_options_page( |
80 | __('BeyondWords Settings', 'speechkit'), |
81 | __('BeyondWords', 'speechkit'), |
82 | 'manage_options', |
83 | 'beyondwords', |
84 | array($this, 'createAdminInterface') |
85 | ); |
86 | } |
87 | |
88 | /** |
89 | * Validate API creds if we are on the credentials tab. |
90 | * |
91 | * @since 5.4.0 |
92 | * |
93 | * @return void |
94 | */ |
95 | public function maybeValidateApiCreds() |
96 | { |
97 | $activeTab = self::getActiveTab(); |
98 | |
99 | if ($activeTab === 'credentials') { |
100 | SettingsUtils::validateApiConnection(); |
101 | } |
102 | } |
103 | |
104 | /** |
105 | * Prints the admin interface for plugin settings. |
106 | * |
107 | * @since 3.0.0 |
108 | * @since 4.7.0 Added tabs. |
109 | * |
110 | * @return void |
111 | */ |
112 | public function createAdminInterface() |
113 | { |
114 | $tabs = self::getTabs(); |
115 | $activeTab = self::getActiveTab(); |
116 | ?> |
117 | <div class="wrap"> |
118 | <h1> |
119 | <?php esc_attr_e('BeyondWords Settings', 'speechkit'); ?> |
120 | </h1> |
121 | |
122 | <form |
123 | id="beyondwords-plugin-settings" |
124 | action="<?php echo esc_url(admin_url('options.php')); ?>" |
125 | method="post" |
126 | > |
127 | <nav class="nav-tab-wrapper"> |
128 | <ul> |
129 | <?php |
130 | foreach ($tabs as $id => $title) { |
131 | $activeClass = $id === $activeTab ? 'nav-tab-active' : ''; |
132 | |
133 | $url = add_query_arg([ |
134 | 'page' => 'beyondwords', |
135 | 'tab' => urlencode($id), |
136 | ]); |
137 | ?> |
138 | <li> |
139 | <a |
140 | class="nav-tab <?php echo esc_attr($activeClass); ?>" |
141 | href="<?php echo esc_url($url); ?>" |
142 | > |
143 | <?php echo wp_kses_post($title); ?> |
144 | </a> |
145 | </li> |
146 | <?php |
147 | } |
148 | ?> |
149 | </ul> |
150 | </nav> |
151 | |
152 | <hr class="wp-header-end"> |
153 | |
154 | <?php |
155 | settings_fields("beyondwords_{$activeTab}_settings"); |
156 | do_settings_sections("beyondwords_{$activeTab}"); |
157 | |
158 | // Some tabs have no fields to submit |
159 | if (! in_array($activeTab, ['summarization', 'pronunciations'])) { |
160 | submit_button('Save changes'); |
161 | } |
162 | ?> |
163 | </form> |
164 | </div> |
165 | <?php |
166 | } |
167 | |
168 | /** |
169 | * Add "Settings" link to plugin page. |
170 | * |
171 | * @since 3.0.0 |
172 | * @since 4.7.0 Prepend custom links instead of appending them. |
173 | */ |
174 | public function addSettingsLinkToPluginPage($links) |
175 | { |
176 | $settingsLink = '<a href="' . |
177 | esc_url(admin_url('options-general.php?page=beyondwords')) . |
178 | '">' . __('Settings', 'speechkit') . '</a>'; |
179 | |
180 | array_unshift($links, $settingsLink); |
181 | |
182 | return $links; |
183 | } |
184 | |
185 | /** |
186 | * Get tabs. |
187 | * |
188 | * @since 4.7.0 |
189 | * @since 5.2.0 Make static. |
190 | * |
191 | * @return array Tabs |
192 | */ |
193 | public static function getTabs() |
194 | { |
195 | $tabs = array( |
196 | 'credentials' => __('Credentials', 'speechkit'), |
197 | 'content' => __('Content', 'speechkit'), |
198 | 'voices' => __('Voices', 'speechkit'), |
199 | 'player' => __('Player', 'speechkit'), |
200 | 'summarization' => __('Summarization', 'speechkit'), |
201 | 'pronunciations' => __('Pronunciations', 'speechkit'), |
202 | ); |
203 | |
204 | if (! SettingsUtils::hasValidApiConnection()) { |
205 | $tabs = array_splice($tabs, 0, 1); |
206 | } |
207 | |
208 | return $tabs; |
209 | } |
210 | |
211 | /** |
212 | * Get active tab. |
213 | * |
214 | * @since 4.7.0 |
215 | * @since 5.2.0 Make static. |
216 | * |
217 | * @return string Active tab |
218 | */ |
219 | public static function getActiveTab() |
220 | { |
221 | $tabs = self::getTabs(); |
222 | |
223 | if (! count($tabs)) { |
224 | return ''; |
225 | } |
226 | |
227 | $defaultTab = array_key_first($tabs); |
228 | |
229 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
230 | if (isset($_GET['tab'])) { |
231 | $tab = sanitize_text_field(wp_unslash($_GET['tab'])); |
232 | } else { |
233 | $tab = $defaultTab; |
234 | } |
235 | // phpcs:enable WordPress.Security.NonceVerification.Recommended |
236 | |
237 | if (!empty($tab) && array_key_exists($tab, $tabs)) { |
238 | $activeTab = $tab; |
239 | } else { |
240 | $activeTab = $defaultTab; |
241 | } |
242 | |
243 | return $activeTab; |
244 | } |
245 | |
246 | /** |
247 | * Print missing API creds warning. |
248 | * |
249 | * @since 5.2.0 |
250 | * |
251 | * @return void |
252 | */ |
253 | public function printMissingApiCredsWarning() |
254 | { |
255 | if (! SettingsUtils::hasApiCreds()) : |
256 | ?> |
257 | <div class="notice notice-info"> |
258 | <p> |
259 | <strong> |
260 | <?php |
261 | printf( |
262 | /* translators: %s is replaced with a "plugin settings" link */ |
263 | esc_html__('To use BeyondWords, please update the %s.', 'speechkit'), |
264 | sprintf( |
265 | '<a href="%s">%s</a>', |
266 | esc_url(admin_url('options-general.php?page=beyondwords')), |
267 | esc_html__('plugin settings', 'speechkit') |
268 | ) |
269 | ); |
270 | ?> |
271 | </strong> |
272 | </p> |
273 | <p> |
274 | <?php esc_html_e('Don’t have a BeyondWords account yet?', 'speechkit'); ?> |
275 | </p> |
276 | <p> |
277 | <a |
278 | class="button button-secondary" |
279 | href="<?php echo esc_url(sprintf('%s/auth/signup', Environment::getDashboardUrl())); ?>" |
280 | target="_blank" |
281 | > |
282 | <?php esc_html_e('Sign up free', 'speechkit'); ?> |
283 | </a> |
284 | </p> |
285 | </div> |
286 | <?php |
287 | endif; |
288 | } |
289 | |
290 | /** |
291 | * Maybe print plugin review notice. |
292 | * |
293 | * @since 5.4.0 |
294 | * |
295 | * @return void |
296 | */ |
297 | public function maybePrintPluginReviewNotice() |
298 | { |
299 | $screen = get_current_screen(); |
300 | if ($screen && 'settings_page_beyondwords' !== $screen->id) { |
301 | return; |
302 | } |
303 | |
304 | $dateActivated = get_option('beyondwords_date_activated', '2025-03-01'); |
305 | $dateNoticeDismissed = get_option('beyondwords_notice_review_dismissed', ''); |
306 | |
307 | $showNotice = false; |
308 | |
309 | if (empty($dateNoticeDismissed)) { |
310 | $dateActivated = strtotime($dateActivated); |
311 | |
312 | if ($dateActivated < strtotime(self::REVIEW_NOTICE_TIME_FORMAT)) { |
313 | $showNotice = true; |
314 | } |
315 | } |
316 | |
317 | if ($showNotice) : |
318 | ?> |
319 | <div id="beyondwords_notice_review" class="notice notice-info is-dismissible"> |
320 | <p> |
321 | <strong> |
322 | <?php |
323 | printf( |
324 | /* translators: %s is replaced with a "WordPress Plugin Repo" link */ |
325 | esc_html__('Happy with our work? Help us spread the word with a rating on the %s.', 'speechkit'), // phpcs:ignore Generic.Files.LineLength.TooLong |
326 | sprintf( |
327 | '<a href="%s">%s</a>', |
328 | 'https://wordpress.org/support/plugin/speechkit/reviews/', |
329 | esc_html__('WordPress Plugin Repo', 'speechkit') |
330 | ) |
331 | ); |
332 | ?> |
333 | </strong> |
334 | </p> |
335 | </div> |
336 | <?php |
337 | endif; |
338 | } |
339 | |
340 | /** |
341 | * Print settings errors. |
342 | * |
343 | * @since 3.0.0 |
344 | * |
345 | * @return void |
346 | */ |
347 | public function printSettingsErrors() |
348 | { |
349 | $settingsErrors = wp_cache_get('beyondwords_settings_errors', 'beyondwords'); |
350 | wp_cache_delete('beyondwords_settings_errors', 'beyondwords'); |
351 | |
352 | if (is_array($settingsErrors) && count($settingsErrors)) : |
353 | ?> |
354 | <div class="notice notice-error"> |
355 | <ul class="ul-disc"> |
356 | <?php |
357 | foreach ($settingsErrors as $error) { |
358 | printf( |
359 | '<li>%s</li>', |
360 | // Only allow links with href and target attributes |
361 | wp_kses( |
362 | $error, |
363 | array( |
364 | 'a' => array( |
365 | 'href' => array(), |
366 | 'target' => array(), |
367 | ), |
368 | 'b' => array(), |
369 | 'strong' => array(), |
370 | 'i' => array(), |
371 | 'em' => array(), |
372 | 'br' => array(), |
373 | 'code' => array(), |
374 | ) |
375 | ) |
376 | ); |
377 | } |
378 | ?> |
379 | </ul> |
380 | </div> |
381 | <?php |
382 | endif; |
383 | } |
384 | |
385 | /** |
386 | * Register WP REST API routes |
387 | * |
388 | * @since 5.4.0 Add REST API route to dismiss review notice. |
389 | * |
390 | * @return void |
391 | */ |
392 | public function restApiInit() |
393 | { |
394 | // settings endpoint |
395 | register_rest_route('beyondwords/v1', '/settings', array( |
396 | 'methods' => \WP_REST_Server::READABLE, |
397 | 'callback' => array($this, 'restApiResponse'), |
398 | 'permission_callback' => function () { |
399 | return current_user_can('edit_posts'); |
400 | }, |
401 | )); |
402 | |
403 | // settings endpoint |
404 | register_rest_route('beyondwords/v1', '/settings', array( |
405 | 'methods' => \WP_REST_Server::READABLE, |
406 | 'callback' => array($this, 'restApiResponse'), |
407 | 'permission_callback' => function () { |
408 | return current_user_can('edit_posts'); |
409 | }, |
410 | )); |
411 | |
412 | // dismiss review notice endpoint |
413 | register_rest_route('beyondwords/v1', '/settings/notices/review/dismiss', array( |
414 | 'methods' => \WP_REST_Server::CREATABLE, |
415 | 'callback' => array($this, 'dismissReviewNotice'), |
416 | 'permission_callback' => function () { |
417 | return current_user_can('manage_options'); |
418 | }, |
419 | )); |
420 | } |
421 | |
422 | /** |
423 | * WP REST API response (required for the Gutenberg editor). |
424 | * |
425 | * DO NOT expose ALL settings e.g. be sure to never expose the API key. |
426 | * |
427 | * @since 3.0.0 |
428 | * @since 3.4.0 Add pluginVersion and wpVersion. |
429 | * |
430 | * @return \WP_REST_Response |
431 | */ |
432 | public function restApiResponse() |
433 | { |
434 | global $wp_version; |
435 | |
436 | return new \WP_REST_Response([ |
437 | 'apiKey' => get_option('beyondwords_api_key', ''), |
438 | 'pluginVersion' => BEYONDWORDS__PLUGIN_VERSION, |
439 | 'projectId' => get_option('beyondwords_project_id', ''), |
440 | 'preselect' => get_option('beyondwords_preselect', PreselectGenerateAudio::DEFAULT_PRESELECT), |
441 | 'projectLanguageCode' => get_option('beyondwords_project_language_code', ''), |
442 | 'projectBodyVoiceId' => get_option('beyondwords_project_body_voice_id', ''), |
443 | 'wpVersion' => $wp_version, |
444 | ]); |
445 | } |
446 | |
447 | /** |
448 | * Dismiss review notice. |
449 | * |
450 | * @since 5.4.0 |
451 | * |
452 | * @return \WP_REST_Response |
453 | */ |
454 | public function dismissReviewNotice() |
455 | { |
456 | $success = update_option('beyondwords_notice_review_dismissed', gmdate(\DateTime::ATOM)); |
457 | |
458 | return new \WP_REST_Response( |
459 | [ |
460 | 'success' => $success |
461 | ], |
462 | $success ? 200 : 500 |
463 | ); |
464 | } |
465 | |
466 | /** |
467 | * Register the settings script. |
468 | * |
469 | * @since 5.0.0 |
470 | * |
471 | * @param string $hook Page hook |
472 | * |
473 | * @return void |
474 | */ |
475 | public function enqueueScripts($hook) |
476 | { |
477 | if ($hook === 'settings_page_beyondwords') { |
478 | // jQuery UI JS |
479 | wp_enqueue_script('jquery-ui-core');// enqueue jQuery UI Core |
480 | wp_enqueue_script('jquery-ui-tabs');// enqueue jQuery UI Tabs |
481 | |
482 | // Plugin settings JS |
483 | wp_register_script( |
484 | 'beyondwords-settings', |
485 | BEYONDWORDS__PLUGIN_URI . 'build/settings.js', |
486 | ['jquery', 'jquery-ui-core', 'jquery-ui-tabs', 'underscore', 'tom-select'], |
487 | BEYONDWORDS__PLUGIN_VERSION, |
488 | true |
489 | ); |
490 | |
491 | // Tom Select JS |
492 | wp_enqueue_script( |
493 | 'tom-select', |
494 | 'https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js', // phpcs:ignore |
495 | [], |
496 | '2.2.2', |
497 | true |
498 | ); |
499 | |
500 | // Plugin settings CSS |
501 | wp_enqueue_style( |
502 | 'beyondwords-settings', |
503 | BEYONDWORDS__PLUGIN_URI . 'src/Component/Settings/settings.css', |
504 | 'forms', |
505 | BEYONDWORDS__PLUGIN_VERSION |
506 | ); |
507 | |
508 | // Tom Select CSS |
509 | wp_enqueue_style( |
510 | 'tom-select', |
511 | 'https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css', // phpcs:ignore |
512 | false, |
513 | BEYONDWORDS__PLUGIN_VERSION |
514 | ); |
515 | |
516 | /** |
517 | * Localize the script to handle ajax requests |
518 | */ |
519 | wp_add_inline_script( |
520 | 'beyondwords-settings', |
521 | ' |
522 | var beyondwordsData = beyondwordsData || {}; |
523 | beyondwordsData.nonce = "' . wp_create_nonce('wp_rest') . '"; |
524 | beyondwordsData.root = "' . esc_url_raw(rest_url()) . '"; |
525 | ', |
526 | 'before', |
527 | ); |
528 | |
529 | wp_enqueue_script('beyondwords-settings'); |
530 | } |
531 | } |
532 | } |