Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.86% covered (warning)
82.86%
116 / 140
55.56% covered (danger)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Metabox
82.73% covered (warning)
82.73%
115 / 139
55.56% covered (danger)
55.56%
5 / 9
20.86
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add_meta_box_callback
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
2.00
 render_meta_box_content
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
4
 heading
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 pending_review_notice
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 player_embed
95.56% covered (success)
95.56%
43 / 45
0.00% covered (danger)
0.00%
0 / 1
5
 errors
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 help
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 regenerate_instructions
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare( strict_types = 1 );
4
5/**
6 * BeyondWords Post Metabox.
7 *
8 * @package BeyondWords\Editor\Classic
9 * @author  Stuart McAlpine <stu@beyondwords.io>
10 * @since   3.0.0
11 * @since   7.0.0 Refactored to BeyondWords namespace with snake_case methods.
12 */
13
14namespace BeyondWords\Editor\Classic;
15
16/**
17 * PostMetabox
18 *
19 * @since 3.0.0
20 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
21 */
22defined( 'ABSPATH' ) || exit;
23
24class Metabox {
25
26    /**
27     * Init.
28     *
29     * @since 4.0.0
30     * @since 6.0.0 Make static.
31     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
32     */
33    public static function init() {
34        add_action( 'add_meta_boxes', [ self::class, 'add_meta_box_callback'] );
35    }
36
37    /**
38     * Adds the meta box container.
39     *
40     * @since 6.0.0 Make static.
41     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
42     *
43     * @param string $post_type
44     */
45    public static function add_meta_box_callback( $post_type ) {
46        $post_types = \BeyondWords\Settings\Utils::get_compatible_post_types();
47
48        if ( ! in_array( $post_type, $post_types ) ) {
49            return;
50        }
51
52        add_meta_box(
53            'beyondwords',
54            __( 'BeyondWords', 'speechkit' ),
55            [ self::class, 'render_meta_box_content'],
56            $post_type,
57            'side',
58            'default',
59            [
60                '__back_compat_meta_box' => true,
61            ]
62        );
63    }
64
65    /**
66     * Render Meta Box content.
67     *
68     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
69     *
70     * @param int|\WP_Post $post The WordPress post ID, or post object.
71     *
72     * @since 3.0.0
73     * @since 3.7.0 Show "Pending review" notice for posts with status of "pending"
74     * @since 4.0.0 Content ID is no longer an int
75     * @since 4.1.0 Add "Player style" and update component display conditions
76     * @since 6.0.0 Make static and add Magic Embed support.
77     */
78    public static function render_meta_box_content( $post ) {
79        $post = get_post( $post );
80
81        if ( ! ( $post instanceof \WP_Post ) ) {
82            return;
83        }
84
85        $has_content = \BeyondWords\Post\Meta::has_content( $post->ID );
86
87        // Single nonce guarding the Content/Format/Player <select> fields.
88        \BeyondWords\Editor\Components\SettingsFields::nonce();
89
90        self::heading( __( 'Player', 'speechkit' ) );
91
92        self::errors( $post );
93
94        if ( $has_content ) {
95            if ( get_post_status( $post ) === 'pending' ) {
96                self::pending_review_notice( $post );
97            } else {
98                self::player_embed( $post );
99            }
100        }
101
102        // The Embed dropdown ("None" = no player) replaces the old Display player checkbox.
103        \BeyondWords\Editor\Components\SettingsFields::render_player_section( $post );
104        ( new \BeyondWords\Editor\Components\GenerateAudio() )::element( $post );
105
106        echo '<hr />';
107        self::heading( __( 'Content', 'speechkit' ) );
108        \BeyondWords\Editor\Components\SettingsFields::render_content_section( $post );
109
110        echo '<hr />';
111        self::heading( __( 'Format', 'speechkit' ) );
112        \BeyondWords\Editor\Components\SettingsFields::render_format_section( $post );
113
114        echo '<hr />';
115        self::heading( __( 'Voice', 'speechkit' ) );
116        ( new \BeyondWords\Editor\Components\SelectVoice() )::element( $post );
117
118        echo '<hr />';
119        self::heading( __( 'Data', 'speechkit' ) );
120        \BeyondWords\Editor\Components\ContentId::element( $post );
121
122        echo '<hr />';
123        self::help();
124    }
125
126    /**
127     * Print a settings-section heading.
128     *
129     * @since 7.0.0
130     *
131     * @param string $title The section heading.
132     */
133    public static function heading( $title ) {
134        printf(
135            '<h4 class="beyondwords-metabox__heading">%s</h4>',
136            esc_html( $title )
137        );
138    }
139
140    /**
141     * The "Pending review" message for posts with "pending" status.
142     *
143     * Shown instead of the player, which cannot render audio created with
144     * { published: false }.
145     *
146     * @since 3.7.0
147     * @since 6.0.0 Make static.
148     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
149     *
150     * @var int|\WP_Post $post The WordPress post ID, or post object.
151     */
152    public static function pending_review_notice( $post ) {
153        $post = get_post( $post );
154
155        if ( ! ( $post instanceof \WP_Post ) ) {
156            return;
157        }
158
159        $project_url = sprintf(
160            '%s/dashboard/project/%d/content',
161            \BeyondWords\Core\Urls::get_dashboard_url(),
162            \BeyondWords\Post\Meta::get_project_id( $post->ID )
163        );
164
165        ?>
166        <div id="beyondwords-pending-review-message">
167            <?php
168            printf(
169                /* translators: %s is replaced with the link to the BeyondWords dashboard */
170                esc_html__( 'Listen to content saved as “Pending” in the %s.', 'speechkit' ),
171                sprintf(
172                    '<a href="%s" target="_blank" rel="nofollow">%s</a>',
173                    esc_url( $project_url ),
174                    esc_html__( 'BeyondWords dashboard', 'speechkit' )
175                )
176            );
177            ?>
178        </div>
179        <?php
180    }
181
182    /**
183     * Embed a player for a WordPress post.
184     *
185     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
186     *
187     * @param int|\WP_Post|null $post (Optional) Post ID, or WP_Post object, or null.
188     *
189     * @since 3.x   Introduced
190     * @since 4.0.1 Admin player init is now all in this one function.
191     * @since 6.0.0 Make static and add Magic Embed support.
192     */
193    public static function player_embed( $post = null ) {
194        $post = get_post( $post );
195
196        if ( ! ( $post instanceof \WP_Post ) ) {
197            return;
198        }
199
200        $project_id  = \BeyondWords\Post\Meta::get_project_id( $post->ID );
201        $has_content = \BeyondWords\Post\Meta::has_content( $post->ID );
202
203        if ( ! $project_id || ! $has_content ) {
204            return;
205        }
206
207        $content_id    = \BeyondWords\Post\Meta::get_content_id( $post->ID );
208        $preview_token = \BeyondWords\Post\Meta::get_preview_token( $post->ID );
209
210        // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
211        if ( ! empty( $content_id ) ) :
212            /*
213             * Still-processing content would 404 (and the CDN would cache it), so
214             * render a loading state and let classic-metabox.js poll until `processed`.
215             * The untrusted content ID / preview token travel as esc_attr()'d data-*
216             * attributes — never interpolated into a JS execution context.
217             */
218            ?>
219            <div
220                id="beyondwords-metabox-player"
221                role="status"
222                aria-live="polite"
223                style="margin: 13px 0;"
224                data-project-id="<?php echo esc_attr( $project_id ); ?>"
225                data-content-id="<?php echo esc_attr( $content_id ); ?>"
226                data-preview-token="<?php echo esc_attr( $preview_token ); ?>"
227            >
228                <span
229                    class="spinner is-active"
230                    style="float: none; margin: 0 8px 0 0;"
231                ></span>
232                <span class="beyondwords-player-loading-text">
233                    <?php esc_html_e( 'Generating…', 'speechkit' ); ?>
234                </span>
235            </div>
236            <script
237                defer
238                src='<?php echo esc_url( \BeyondWords\Core\Urls::get_js_sdk_url() ); ?>'
239            ></script>
240            <?php
241        else :
242            /*
243             * Client-side integration is keyed on the source ID — nothing to poll, so
244             * embed immediately. The untrusted preview token is JSON_HEX_*-encoded so
245             * no value breaks out of the onload attribute. Mirrors Javascript::render().
246             */
247            $config = [
248                'projectId'        => (int) $project_id,
249                'sourceId'         => (string) $post->ID,
250                'previewToken'     => (string) $preview_token,
251                'adverts'          => [],
252                'analyticsConsent' => 'none',
253                'introsOutros'     => [],
254                'playerStyle'      => 'small',
255                'widgetStyle'      => 'none',
256            ];
257
258            $onload = sprintf(
259                'const player = new BeyondWords.Player({ target: this.parentElement, ...%s });',
260                wp_json_encode(
261                    $config,
262                    JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
263                )
264            );
265            ?>
266            <div id="beyondwords-metabox-player" style="margin: 13px 0;">
267            <script defer
268                src='<?php echo esc_url( \BeyondWords\Core\Urls::get_js_sdk_url() ); ?>'
269                onload='<?php echo esc_attr( $onload ); ?>'
270            >
271            </script>
272            </div>
273            <?php
274        endif;
275        // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript
276    }
277
278    /**
279     * Display errors for the post.
280     *
281     * @since 6.0.0 Make static.
282     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
283     */
284    public static function errors( $post ) {
285        $error = \BeyondWords\Post\Meta::get_error_message( $post->ID );
286
287        if ( $error ) :
288            ?>
289            <div id="beyondwords-metabox-errors">
290                <div class="beyondwords-error">
291                    <p>
292                        <?php echo esc_html( $error ); ?>
293                    </p>
294                </div>
295                <?php self::regenerate_instructions(); ?>
296            </div>
297            <?php
298        endif;
299    }
300
301    /**
302     * Display help text for the metabox.
303     *
304     * @since 6.0.0 Make static.
305     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
306     */
307    public static function help() {
308        ?>
309        <p id="beyondwords-metabox-help">
310            <?php
311            printf(
312                /* translators: %s is replaced with the link to the support email address */
313                esc_html__( 'Need help? Email our support team on %s', 'speechkit' ),
314                sprintf( '<a href="%s">%s</a>', 'mailto:support@beyondwords.io', 'support@beyondwords.io' )
315            );
316            ?>
317        </p>
318        <?php
319    }
320
321    /**
322     * Display instructions for regenerating audio.
323     *
324     * @since 6.0.0 Make static.
325     * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
326     */
327    public static function regenerate_instructions() {
328        ?>
329        <!-- Update/regenerate -->
330        <p>
331            <?php
332            esc_html_e(
333                'To create audio, resolve the error above then select ‘Update’ with ‘Generate audio’ checked.', // phpcs:ignore Generic.Files.LineLength.TooLong
334                'speechkit'
335            );
336            ?>
337        </p>
338        <?php
339    }
340}