Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.62% covered (warning)
65.62%
84 / 128
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SelectVoice
65.62% covered (warning)
65.62%
84 / 128
14.29% covered (danger)
14.29%
1 / 7
59.85
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 element
96.67% covered (success)
96.67%
58 / 60
0.00% covered (danger)
0.00%
0 / 1
9
 save
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
10.27
 restApiInit
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 languagesRestApiResponse
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 voicesRestApiResponse
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 adminEnqueueScripts
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare(strict_types=1);
4
5/**
6 * BeyondWords Component: Select Voice
7 *
8 * @package Beyondwords\Wordpress
9 * @author  Stuart McAlpine <stu@beyondwords.io>
10 * @since   4.0.0
11 */
12
13namespace Beyondwords\Wordpress\Component\Post\SelectVoice;
14
15use Beyondwords\Wordpress\Core\CoreUtils;
16use Beyondwords\Wordpress\Component\Settings\SettingsUtils;
17use Beyondwords\Wordpress\Core\ApiClient;
18
19/**
20 * SelectVoice
21 *
22 * @since 4.0.0
23 */
24class SelectVoice
25{
26    /**
27     * Init.
28     *
29     * @since 4.0.0
30     */
31    public function init()
32    {
33        add_action('rest_api_init', array($this, 'restApiInit'));
34        add_action('admin_enqueue_scripts', array($this, 'adminEnqueueScripts'));
35
36        add_action('wp_loaded', function () {
37            $postTypes = SettingsUtils::getCompatiblePostTypes();
38
39            if (is_array($postTypes)) {
40                foreach ($postTypes as $postType) {
41                    add_action("save_post_{$postType}", array($this, 'save'), 10);
42                }
43            }
44        });
45    }
46
47    /**
48     * HTML output for this component.
49     *
50     * @since 4.0.0
51     * @since 4.5.1 Hide element if no language data exists.
52     * @since 5.4.0 Always display all languages and associated voices.
53     *
54     * @param WP_Post $post The post object.
55     *
56     * @return string|null
57     */
58    public function element($post)
59    {
60        $postLanguageCode = get_post_meta($post->ID, 'beyondwords_language_code', true);
61        $postVoiceId      = get_post_meta($post->ID, 'beyondwords_body_voice_id', true);
62
63        $languageCode = $postLanguageCode ?: get_option('beyondwords_project_language_code');
64        $voiceId      = $postVoiceId ?: get_option('beyondwords_project_body_voice_id');
65
66        $languages = ApiClient::getLanguages();
67        $voices    = ApiClient::getVoices($languageCode);
68
69        if (! is_array($voices)) {
70            $voices = [];
71        }
72
73        wp_nonce_field('beyondwords_select_voice', 'beyondwords_select_voice_nonce');
74        ?>
75        <p
76            id="beyondwords-metabox-select-voice--language-code"
77            class="post-attributes-label-wrapper page-template-label-wrapper"
78        >
79            <label class="post-attributes-label" for="beyondwords_language_code">
80                Language
81            </label>
82        </p>
83        <select id="beyondwords_language_code" name="beyondwords_language_code" style="width: 100%;">
84            <?php
85            foreach ($languages as $language) {
86                if (empty($language['code']) || empty($language['name'])  || empty($language['accent'])) {
87                    continue;
88                }
89                printf(
90                    '<option value="%s" data-default-voice-id="%s" %s>%s (%s)</option>',
91                    esc_attr($language['code']),
92                    esc_attr($language['default_voices']['body']['id'] ?? ''),
93                    selected(strval($language['code']), strval($languageCode)),
94                    esc_html($language['name']),
95                    esc_html($language['accent'])
96                );
97            }
98            ?>
99        </select>
100        <p
101            id="beyondwords-metabox-select-voice--voice-id"
102            class="post-attributes-label-wrapper page-template-label-wrapper"
103        >
104            <label class="post-attributes-label" for="beyondwords_voice_id">
105                Voice
106            </label>
107        </p>
108        <select
109            id="beyondwords_voice_id"
110            name="beyondwords_voice_id"
111            style="width: 100%;"
112            <?php echo disabled(!strval($languageCode)) ?>
113        >
114            <?php
115            foreach ($voices as $voice) {
116                printf(
117                    '<option value="%s" %s>%s</option>',
118                    esc_attr($voice['id']),
119                    selected(strval($voice['id']), strval($voiceId)),
120                    esc_html($voice['name'])
121                );
122            }
123            ?>
124        </select>
125        <img
126            src="/wp-admin/images/spinner.gif"
127            class="beyondwords-settings__loader"
128            style="display:none; padding: 3px 0;"
129        />
130        <?php
131    }
132
133    /**
134     * Save the meta when the post is saved.
135     *
136     * @since 4.0.0
137     *
138     * @param int $postId The ID of the post being saved.
139     */
140    public function save($postId)
141    {
142        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
143            return $postId;
144        }
145
146        // "save_post" can be triggered at other times, so verify this request came from the our component
147        if (
148            ! isset($_POST['beyondwords_language_code']) ||
149            ! isset($_POST['beyondwords_voice_id']) ||
150            ! isset($_POST['beyondwords_select_voice_nonce'])
151        ) {
152            return $postId;
153        }
154
155        // "save_post" can be triggered at other times, so verify this request came from the our component
156        if (
157            ! wp_verify_nonce(
158                sanitize_key($_POST['beyondwords_select_voice_nonce']),
159                'beyondwords_select_voice'
160            )
161        ) {
162            return $postId;
163        }
164
165        $languageCode = sanitize_text_field(wp_unslash($_POST['beyondwords_language_code']));
166
167        if (! empty($languageCode)) {
168            update_post_meta($postId, 'beyondwords_language_code', $languageCode);
169        } else {
170            delete_post_meta($postId, 'beyondwords_language_code');
171        }
172
173        $voiceId = sanitize_text_field(wp_unslash($_POST['beyondwords_voice_id']));
174
175        if (! empty($voiceId)) {
176            update_post_meta($postId, 'beyondwords_body_voice_id', $voiceId);
177            update_post_meta($postId, 'beyondwords_title_voice_id', $voiceId);
178            update_post_meta($postId, 'beyondwords_summary_voice_id', $voiceId);
179        } else {
180            delete_post_meta($postId, 'beyondwords_body_voice_id');
181            delete_post_meta($postId, 'beyondwords_title_voice_id');
182            delete_post_meta($postId, 'beyondwords_summary_voice_id');
183        }
184
185        return $postId;
186    }
187
188    /**
189     * Register WP REST API route
190     *
191     * @since 4.0.0
192     *
193     * @return void
194     */
195    public function restApiInit()
196    {
197        // Languages endpoint
198        register_rest_route('beyondwords/v1', '/languages', array(
199            'methods'  => \WP_REST_Server::READABLE,
200            'callback' => array($this, 'languagesRestApiResponse'),
201            'permission_callback' => function () {
202                return current_user_can('edit_posts');
203            },
204        ));
205
206        // Voices endpoint
207        register_rest_route('beyondwords/v1', '/languages/(?P<languageCode>[a-zA-Z0-9-_]+)/voices', array(
208            'methods'  => \WP_REST_Server::READABLE,
209            'callback' => array($this, 'voicesRestApiResponse'),
210            'permission_callback' => function () {
211                return current_user_can('edit_posts');
212            },
213        ));
214    }
215
216    /**
217     * "Languages" WP REST API response (required for the Gutenberg editor).
218     *
219     * @since 4.0.0
220     * @since 5.4.0 No longer filter by "Languages" plugin setting.
221     *
222     * @return \WP_REST_Response
223     */
224    public function languagesRestApiResponse()
225    {
226        $languages = ApiClient::getLanguages();
227
228        return new \WP_REST_Response($languages);
229    }
230
231    /**
232     * "Voices" WP REST API response (required for the Gutenberg editor
233     * and Block Editor).
234     *
235     * @since 4.0.0
236     *
237     * @return \WP_REST_Response
238     */
239    public function voicesRestApiResponse(\WP_REST_Request $data)
240    {
241        $params = $data->get_url_params();
242
243        $voices = ApiClient::getVoices($params['languageCode']);
244
245        return new \WP_REST_Response($voices);
246    }
247
248    /**
249     * Register the component scripts.
250     *
251     * @since  4.0.0
252     *
253     * @param string $hook Page hook
254     *
255     * @return void
256     */
257    public function adminEnqueueScripts($hook)
258    {
259        if (! CoreUtils::isGutenbergPage() && ( $hook === 'post.php' || $hook === 'post-new.php')) {
260            wp_register_script(
261                'beyondwords-metabox--select-voice',
262                BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/SelectVoice/classic-metabox.js',
263                ['jquery', 'underscore'],
264                BEYONDWORDS__PLUGIN_VERSION,
265                true
266            );
267
268            /**
269             * Localize the script to handle ajax requests
270             */
271            wp_localize_script(
272                'beyondwords-metabox--select-voice',
273                'beyondwordsData',
274                array(
275                    'nonce' => wp_create_nonce('wp_rest'),
276                    'root' => esc_url_raw(rest_url()),
277                )
278            );
279
280            wp_enqueue_script('beyondwords-metabox--select-voice');
281        }
282    }
283}