Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
187 / 187
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
Fields
100.00% covered (success)
100.00%
187 / 187
100.00% covered (success)
100.00%
16 / 16
26
100.00% covered (success)
100.00%
1 / 1
 init
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 register_authentication_fields
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
1
 register_integration_fields
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 register_preferences_fields
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
1
 sanitize_api_key
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 sanitize_project_id
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 sanitize_integration_method
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 sanitize_player_ui
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 render_api_key
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 render_project_id
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 render_integration_method
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
 render_prepend_excerpt
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 render_player_ui
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
 get_integration_method
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 get_integration_method_options
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_player_ui_options
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * BeyondWords settings fields.
4 *
5 * Compound fields (e.g. preselect) live in their own classes.
6 *
7 * @package BeyondWords\Settings
8 *
9 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
10 */
11
12declare( strict_types = 1 );
13
14namespace BeyondWords\Settings;
15
16defined( 'ABSPATH' ) || exit;
17
18/**
19 * Settings fields.
20 *
21 * Option keys and value enums are class constants so the rest of the plugin
22 * can read them without going through the settings page.
23 *
24 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
25 */
26class Fields {
27
28    const OPTION_API_KEY            = 'beyondwords_api_key';
29    const OPTION_PROJECT_ID         = 'beyondwords_project_id';
30    const OPTION_INTEGRATION_METHOD = 'beyondwords_integration_method';
31    const OPTION_PREPEND_EXCERPT    = 'beyondwords_prepend_excerpt';
32    const OPTION_PLAYER_UI          = 'beyondwords_player_ui';
33
34    const INTEGRATION_REST_API    = 'rest-api';
35    const INTEGRATION_CLIENT_SIDE = 'client-side';
36
37    const PLAYER_UI_ENABLED  = 'enabled';
38    const PLAYER_UI_HEADLESS = 'headless';
39    const PLAYER_UI_DISABLED = 'disabled';
40
41    /**
42     * Register WordPress hooks.
43     */
44    public static function init(): void {
45        add_action( 'admin_init', [ self::class, 'register_authentication_fields' ] );
46        add_action( 'admin_init', [ self::class, 'register_integration_fields' ] );
47        add_action( 'admin_init', [ self::class, 'register_preferences_fields' ] );
48    }
49
50    /**
51     * Register API key + project ID for the Authentication tab.
52     */
53    public static function register_authentication_fields(): void {
54        register_setting(
55            Tabs::SETTINGS_GROUP_AUTHENTICATION,
56            self::OPTION_API_KEY,
57            [
58                'type'              => 'string',
59                'default'           => '',
60                'sanitize_callback' => [ self::class, 'sanitize_api_key' ],
61            ]
62        );
63
64        register_setting(
65            Tabs::SETTINGS_GROUP_AUTHENTICATION,
66            self::OPTION_PROJECT_ID,
67            [
68                'type'              => 'string',
69                'default'           => '',
70                'sanitize_callback' => [ self::class, 'sanitize_project_id' ],
71            ]
72        );
73
74        add_settings_field(
75            'beyondwords-api-key',
76            __( 'API key', 'speechkit' ),
77            [ self::class, 'render_api_key' ],
78            Tabs::PAGE_AUTHENTICATION,
79            Tabs::SECTION_AUTHENTICATION
80        );
81
82        add_settings_field(
83            'beyondwords-project-id',
84            __( 'Project ID', 'speechkit' ),
85            [ self::class, 'render_project_id' ],
86            Tabs::PAGE_AUTHENTICATION,
87            Tabs::SECTION_AUTHENTICATION
88        );
89    }
90
91    /**
92     * Register the integration method field for the Integration tab.
93     */
94    public static function register_integration_fields(): void {
95        register_setting(
96            Tabs::SETTINGS_GROUP_INTEGRATION,
97            self::OPTION_INTEGRATION_METHOD,
98            [
99                'type'              => 'string',
100                'default'           => self::INTEGRATION_REST_API,
101                'sanitize_callback' => [ self::class, 'sanitize_integration_method' ],
102            ]
103        );
104
105        add_settings_field(
106            'beyondwords-integration-method',
107            __( 'Integration method', 'speechkit' ),
108            [ self::class, 'render_integration_method' ],
109            Tabs::PAGE_INTEGRATION,
110            Tabs::SECTION_INTEGRATION
111        );
112    }
113
114    /**
115     * Register the per-site preferences (excerpt, player UI).
116     *
117     * Preselect lives in its own class: post-type-aware logic plus a JS asset.
118     */
119    public static function register_preferences_fields(): void {
120        register_setting(
121            Tabs::SETTINGS_GROUP_PREFERENCES,
122            self::OPTION_PREPEND_EXCERPT,
123            [
124                'type'              => 'boolean',
125                'default'           => false,
126                'sanitize_callback' => 'rest_sanitize_boolean',
127            ]
128        );
129        add_filter( 'option_' . self::OPTION_PREPEND_EXCERPT, 'rest_sanitize_boolean' );
130
131        register_setting(
132            Tabs::SETTINGS_GROUP_PREFERENCES,
133            self::OPTION_PLAYER_UI,
134            [
135                'type'              => 'string',
136                'default'           => self::PLAYER_UI_ENABLED,
137                'sanitize_callback' => [ self::class, 'sanitize_player_ui' ],
138            ]
139        );
140
141        add_settings_field(
142            'beyondwords-include-excerpt',
143            __( 'Excerpt', 'speechkit' ),
144            [ self::class, 'render_prepend_excerpt' ],
145            Tabs::PAGE_PREFERENCES,
146            Tabs::SECTION_PREFERENCES
147        );
148
149        add_settings_field(
150            'beyondwords-player-ui',
151            __( 'Player UI', 'speechkit' ),
152            [ self::class, 'render_player_ui' ],
153            Tabs::PAGE_PREFERENCES,
154            Tabs::SECTION_PREFERENCES
155        );
156    }
157
158    /**
159     * Sanitisers
160     * ------------------------------------------------------------------ */
161    public static function sanitize_api_key( $value ) {
162        $value = sanitize_text_field( (string) $value );
163
164        if ( '' === $value ) {
165            Utils::add_settings_error_message(
166                __( 'Please enter the BeyondWords API key. This can be found in your project settings.', 'speechkit' ),
167                'Settings/ApiKey'
168            );
169        }
170
171        return $value;
172    }
173
174    public static function sanitize_project_id( $value ) {
175        $value = sanitize_text_field( (string) $value );
176
177        if ( '' === $value ) {
178            Utils::add_settings_error_message(
179                __( 'Please enter your BeyondWords project ID. This can be found in your project settings.', 'speechkit' ),
180                'Settings/ProjectId'
181            );
182        }
183
184        return $value;
185    }
186
187    public static function sanitize_integration_method( $value ): string {
188        $value = is_string( $value ) ? $value : '';
189        return in_array( $value, [ self::INTEGRATION_REST_API, self::INTEGRATION_CLIENT_SIDE ], true )
190            ? $value
191            : self::INTEGRATION_REST_API;
192    }
193
194    public static function sanitize_player_ui( $value ): string {
195        $value = is_string( $value ) ? $value : '';
196        return in_array( $value, [ self::PLAYER_UI_ENABLED, self::PLAYER_UI_HEADLESS, self::PLAYER_UI_DISABLED ], true )
197            ? $value
198            : self::PLAYER_UI_ENABLED;
199    }
200
201    /**
202     * Renderers
203     * ------------------------------------------------------------------ */
204    public static function render_api_key(): void {
205        $value = (string) get_option( self::OPTION_API_KEY, '' );
206        ?>
207        <input
208            type="text"
209            id="<?php echo esc_attr( self::OPTION_API_KEY ); ?>"
210            name="<?php echo esc_attr( self::OPTION_API_KEY ); ?>"
211            value="<?php echo esc_attr( $value ); ?>"
212            size="50"
213        />
214        <?php
215    }
216
217    public static function render_project_id(): void {
218        $value = (string) get_option( self::OPTION_PROJECT_ID, '' );
219        ?>
220        <input
221            type="text"
222            id="<?php echo esc_attr( self::OPTION_PROJECT_ID ); ?>"
223            name="<?php echo esc_attr( self::OPTION_PROJECT_ID ); ?>"
224            value="<?php echo esc_attr( $value ); ?>"
225            size="10"
226        />
227        <?php
228    }
229
230    public static function render_integration_method(): void {
231        $current = self::get_integration_method();
232        ?>
233        <div class="beyondwords-setting__integration-method">
234            <select name="<?php echo esc_attr( self::OPTION_INTEGRATION_METHOD ); ?>" id="<?php echo esc_attr( self::OPTION_INTEGRATION_METHOD ); ?>">
235                <?php foreach ( self::get_integration_method_options() as $value => $label ) : ?>
236                    <option value="<?php echo esc_attr( $value ); ?><?php selected( $value, $current ); ?>>
237                        <?php echo esc_html( $label ); ?>
238                    </option>
239                <?php endforeach; ?>
240            </select>
241        </div>
242        <p class="description">
243            <?php
244            printf(
245                /* translators: %s is the link text "Magic Embed" */
246                esc_html__( 'REST API is the default integration. Choose %s if your theme/plugins prevent BeyondWords from saving content via the REST API â€” for example on sites using a page builder.', 'speechkit' ),
247                sprintf(
248                    '<a href="https://github.com/beyondwords-io/player/blob/main/doc/client-side-integration.md" target="_blank" rel="nofollow">%s</a>',
249                    esc_html__( 'Magic Embed', 'speechkit' )
250                )
251            );
252            ?>
253        </p>
254        <?php
255    }
256
257    public static function render_prepend_excerpt(): void {
258        $value = (bool) get_option( self::OPTION_PREPEND_EXCERPT, false );
259        ?>
260        <div>
261            <label>
262                <input type="hidden" name="<?php echo esc_attr( self::OPTION_PREPEND_EXCERPT ); ?>" value="" />
263                <input
264                    type="checkbox"
265                    id="<?php echo esc_attr( self::OPTION_PREPEND_EXCERPT ); ?>"
266                    name="<?php echo esc_attr( self::OPTION_PREPEND_EXCERPT ); ?>"
267                    value="1"
268                    <?php checked( $value ); ?>
269                />
270                <?php esc_html_e( 'Include the post excerpt at the start of generated audio and video.', 'speechkit' ); ?>
271            </label>
272        </div>
273        <?php
274    }
275
276    public static function render_player_ui(): void {
277        $current = (string) get_option( self::OPTION_PLAYER_UI, self::PLAYER_UI_ENABLED );
278        ?>
279        <div class="beyondwords-setting__player-ui">
280            <select name="<?php echo esc_attr( self::OPTION_PLAYER_UI ); ?>" id="<?php echo esc_attr( self::OPTION_PLAYER_UI ); ?>">
281                <?php foreach ( self::get_player_ui_options() as $value => $label ) : ?>
282                    <option value="<?php echo esc_attr( $value ); ?><?php selected( $value, $current ); ?>>
283                        <?php echo esc_html( $label ); ?>
284                    </option>
285                <?php endforeach; ?>
286            </select>
287        </div>
288        <p class="description">
289            <?php
290            printf(
291                /* translators: %s is the link text "headless mode" */
292                esc_html__( 'Enable or disable the player, or set it to %s.', 'speechkit' ),
293                sprintf(
294                    '<a href="https://github.com/beyondwords-io/player/blob/gh-pages/doc/building-your-own-ui.md" target="_blank" rel="nofollow">%s</a>',
295                    esc_html__( 'headless mode', 'speechkit' )
296                )
297            );
298            ?>
299        </p>
300        <?php
301    }
302
303    /**
304     * Public accessors
305     * ------------------------------------------------------------------ */
306
307    /**
308     * Resolve the integration method for a post (or the site default).
309     *
310     * @param \WP_Post|false $post Post object, or false to get the site default.
311     */
312    public static function get_integration_method( $post = false ): string {
313        $method = '';
314
315        if ( $post instanceof \WP_Post ) {
316            $method = (string) get_post_meta( $post->ID, self::OPTION_INTEGRATION_METHOD, true );
317        }
318
319        if ( '' === $method ) {
320            $method = (string) get_option( self::OPTION_INTEGRATION_METHOD, self::INTEGRATION_REST_API );
321        }
322
323        return self::sanitize_integration_method( $method );
324    }
325
326    /**
327     * Available integration method choices.
328     *
329     * @return array<string,string>
330     */
331    public static function get_integration_method_options(): array {
332        return [
333            self::INTEGRATION_REST_API    => __( 'REST API', 'speechkit' ),
334            self::INTEGRATION_CLIENT_SIDE => __( 'Magic Embed', 'speechkit' ),
335        ];
336    }
337
338    /**
339     * Available player UI choices.
340     *
341     * @return array<string,string>
342     */
343    public static function get_player_ui_options(): array {
344        return [
345            self::PLAYER_UI_ENABLED  => __( 'Enabled', 'speechkit' ),
346            self::PLAYER_UI_HEADLESS => __( 'Headless', 'speechkit' ),
347            self::PLAYER_UI_DISABLED => __( 'Disabled', 'speechkit' ),
348        ];
349    }
350}