Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
PlayerInline
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 13
2352
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 registerShortcodes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 playerShortcode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 autoPrependPlayer
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 playerHtml
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 hasCustomPlayer
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 jsPlayerHtml
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 ampPlayerHtml
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 isPlayerEnabled
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 useAmpPlayer
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 usePlayerJsSdk
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
90
 jsPlayerParams
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 addPluginSettingsToSdkParams
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare(strict_types=1);
4
5namespace Beyondwords\Wordpress\Core\Player;
6
7use Beyondwords\Wordpress\Component\Post\PostMetaUtils;
8use Beyondwords\Wordpress\Component\Settings\Fields\PlayerUI\PlayerUI;
9use Beyondwords\Wordpress\Core\Environment;
10use Beyondwords\Wordpress\Core\CoreUtils;
11use Symfony\Component\DomCrawler\Crawler;
12
13/**
14 * The BeyondWords Player.
15 *
16 * This is an alternate Player class using the inline script method that
17 * is recommended in the player docs.
18 *
19 * @link https://github.com/beyondwords-io/player/blob/main/doc/getting-started.md.
20 **/
21class PlayerInline
22{
23    /**
24     * Init.
25     */
26    public function init()
27    {
28        // Actions
29        add_action('init', array($this, 'registerShortcodes'));
30
31        // Filters
32        add_filter('the_content', array($this, 'autoPrependPlayer'), 1000000);
33        add_filter('newsstand_the_content', array($this, 'autoPrependPlayer'));
34    }
35
36    /**
37     * Register shortcodes.
38     *
39     * @since 4.2.0
40     */
41    public function registerShortcodes()
42    {
43        add_shortcode('beyondwords_player', array($this, 'playerShortcode'));
44    }
45
46    /**
47     * HTML output for the BeyondWords player shortcode.
48     *
49     * @since 4.2.0
50     *
51     * @param array $atts Shortcode attributes.
52     *
53     * @return string
54     */
55    public function playerShortcode()
56    {
57        return $this->playerHtml();
58    }
59
60    /**
61     * Auto-prepends the BeyondWords player to WordPress content.
62     *
63     * @since 3.0.0
64     * @since 4.2.0 Renamed from addPlayerToContent to autoPrependPlayer.
65     * @since 4.2.0 Perform hasCustomPlayer() check here.
66     * @since 4.6.1 Only auto-prepend player for frontend is_singular screens.
67     *
68     * @param string $content WordPress content.
69     *
70     * @return string
71     */
72    public function autoPrependPlayer($content)
73    {
74        if (! is_singular()) {
75            return $content;
76        }
77
78        if ($this->hasCustomPlayer($content)) {
79            return $content;
80        }
81
82        return $this->playerHtml() . $content;
83    }
84
85    /**
86     * Player HTML.
87     *
88     * Displays JS SDK variant of the BeyondWords audio player, for both
89     * AMP and non-AMP content.
90     *
91     * @param WP_Post $post WordPress Post.
92     *
93     * @since 3.0.0
94     * @since 3.1.0 Added _doing_it_wrong deprecation warnings
95     *
96     * @return string
97     */
98    public function playerHtml($post = false)
99    {
100        if (! ($post instanceof \WP_Post)) {
101            $post = get_post($post);
102        }
103
104        if (! $post) {
105            return '';
106        }
107
108        if (! $this->isPlayerEnabled($post)) {
109            return '';
110        }
111
112        $projectId = PostMetaUtils::getProjectId($post->ID);
113
114        if (! $projectId) {
115            return '';
116        }
117
118        $contentId = PostMetaUtils::getContentId($post->ID);
119
120        if (! $contentId) {
121            return '';
122        }
123
124        // AMP or JS Player?
125        if ($this->useAmpPlayer()) {
126            $html = $this->ampPlayerHtml($post->ID, $projectId, $contentId);
127        } else {
128            $html = $this->jsPlayerHtml($post->ID, $projectId, $contentId);
129        }
130
131        /**
132         * Filters the HTML of the BeyondWords Player.
133         *
134         * @since 4.0.0
135         * @since 4.3.0 Applied to both AMP and no-AMP content.
136         *
137         * @param string $html      The HTML for the JS audio player. The audio player JavaScript may
138         *                          fail to locate the target element if you remove or replace the
139         *                          default contents of this parameter.
140         * @param int    $postId    WordPress post ID.
141         * @param int    $projectId BeyondWords project ID.
142         * @param int    $contentId BeyondWords content ID.
143         */
144        $html = apply_filters('beyondwords_player_html', $html, $post->ID, $projectId, $contentId);
145
146        return $html;
147    }
148
149    /**
150     * Has custom player?
151     *
152     * Checks the post content to see whether a custom player has been added.
153     *
154     * @since 3.2.0
155     * @since 4.2.0 Pass $content as a parameter, check for [beyondwords_player] shortcode
156     * @since 4.2.4 Check $content is a string
157     *
158     * @param string $content WordPress content.
159     *
160     * @return boolean
161     */
162    public function hasCustomPlayer($content)
163    {
164        if (! is_string($content)) {
165            return false;
166        }
167
168        if (strpos($content, '[beyondwords_player]') !== false) {
169            return true;
170        }
171
172        $crawler = new Crawler($content);
173
174        return count($crawler->filterXPath('//div[@data-beyondwords-player="true"]')) > 0;
175    }
176
177    /**
178     * JS Player HTML.
179     *
180     * Displays the HTML required for the JS player.
181     *
182     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
183     *
184     * @param int $postId    WordPress Post ID.
185     * @param int $projectId BeyondWords Project ID.
186     * @param int $contentId BeyondWords Content ID.
187     *
188     * @since 3.0.0
189     * @since 3.1.0 Added speechkit_js_player_html filter
190     * @since 4.2.0 Remove hasCustomPlayer() check from here.
191     * @since 5.2.0 Replace div[data-beyondwords-player] with script[onload]
192     * @since 5.3.0 Use new jsPlayerParams() object return.
193     *
194     * @return string
195     */
196    public function jsPlayerHtml($postId, $projectId, $contentId)
197    {
198        if (! $this->usePlayerJsSdk()) {
199            return '';
200        }
201
202        $post   = get_post($postId);
203        $params = $this->jsPlayerParams($post);
204
205        $playerUI = get_option('beyondwords_player_ui', PlayerUI::ENABLED);
206
207        $params->projectId = $projectId;
208        $params->contentId = $contentId;
209
210        $jsonParams = wp_json_encode($params, JSON_UNESCAPED_SLASHES);
211
212        // Headless instantiates a player without a target
213        if ($playerUI !== PlayerUI::HEADLESS) {
214            $jsonParams = sprintf('{...%s, target:this}', $jsonParams);
215        }
216
217        $onload = sprintf('new BeyondWords.Player(%s);', $jsonParams);
218
219        /**
220         * Filters the onload attribute of the BeyondWords Player script.
221         *
222         * Note that to support multiple players on one page, the
223         * default script uses `document.querySelectorAll() to target all
224         * instances of `div[data-beyondwords-player]` in the HTML source.
225         * If this approach is removed then multiple occurrences of the
226         * BeyondWords player in one page may not work as expected.
227         *
228         * @link https://github.com/beyondwords-io/player/blob/main/doc/getting-started.md#how-to-configure-it
229         *
230         * @since 4.0.0
231         *
232         * @param string $script The string value of the onload script.
233         * @param array  $params The SDK params for the current post, including
234         *                       `projectId` and `contentId`.
235         */
236        $onload = apply_filters('beyondwords_player_script_onload', $onload, $params);
237
238        $html = sprintf(
239            // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
240            '<script async defer src="%s" onload=\'%s\'></script>',
241            Environment::getJsSdkUrl(),
242            $onload
243        );
244
245        return $html;
246    }
247
248    /**
249     * AMP Player HTML.
250     *
251     * Displays the HTML required for the AMP player.
252     *
253     * @param int $postId    WordPress Post ID.
254     * @param int $projectId BeyondWords Project ID.
255     * @param int $contentId BeyondWords Content ID.
256     *
257     * @since 3.0.0
258     * @since 3.1.0 Added speechkit_amp_player_html filter
259     *
260     * @return string
261     */
262    public function ampPlayerHtml($postId, $projectId, $contentId)
263    {
264        $src = sprintf(Environment::getAmpPlayerUrl(), $projectId, $contentId);
265
266        // Turn on output buffering
267        ob_start();
268
269        ?>
270        <amp-iframe
271            frameborder="0"
272            height="43"
273            layout="responsive"
274            sandbox="allow-scripts allow-same-origin allow-popups"
275            scrolling="no"
276            src="<?php echo esc_url($src); ?>"
277            width="295"
278        >
279            <amp-img
280                height="150"
281                layout="responsive"
282                placeholder
283                src="<?php echo esc_url(Environment::getAmpImgUrl()); ?>"
284                width="643"
285            ></amp-img>
286        </amp-iframe>
287        <?php
288
289        $html = ob_get_clean();
290
291        /**
292         * Filters the HTML of the BeyondWords AMP audio player.
293         *
294         * This filter is scheduled to be removed in v5.0.
295         *
296         * @since 3.3.3
297         *
298         * @deprecated 4.3.0 beyondwords_player_html is now applied to AMP and non-AMP content.
299         * @see Beyondwords\Wordpress\Core\Player\Player::playerHtml()
300         *
301         * @param string $html       The HTML for the AMP audio player.
302         * @param int    $post_id    WordPress Post ID.
303         * @param int    $project_id BeyondWords Project ID.
304         * @param int    $contentId  BeyondWords Content ID.
305         */
306        $html = apply_filters('beyondwords_amp_player_html', $html, $postId, $projectId, $contentId);
307
308        return $html;
309    }
310
311    /**
312     * Should we show the BeyondWords audio player?
313     *
314     * We DO NOT want to show the player if:
315     * 1. BeyondWords has been disabled in our plugin settings.
316     * 2. The current post type has not been selected in our plugin settings.
317     * 3. The current post has specifically been disabled from processing.
318     *
319     * The return value of this can be overriden with the WordPress
320     * "beyondwords_post_player_enabled" filter.
321     *
322     * @param int|WP_Post (Optional) Post ID or WP_Post object. Default is global $post.
323     *
324     * @since 3.0.0
325     * @since 3.3.4 Accept int|WP_Post as method parameter.
326     * @since 4.0.0 Check beyondwords_player_ui custom field.
327     * @since 5.0.0 Remove beyondwords_post_player_enabled filter.
328     *
329     * @return bool
330     **/
331    public function isPlayerEnabled($post = null)
332    {
333        $post = get_post($post);
334
335        if (! ($post instanceof \WP_Post)) {
336            return false;
337        }
338
339        // Assume we can show the player
340        $enabled = true;
341
342        // Has 'Display Player' been unchecked?
343        if (PostMetaUtils::getDisabled($post->ID)) {
344            $enabled = false;
345        }
346
347        // Is the player ui enabled in plugin settings?
348        if ($enabled) {
349            $enabled = get_option('beyondwords_player_ui', PlayerUI::ENABLED) === PlayerUI::ENABLED;
350        }
351
352        return $enabled;
353    }
354
355    /**
356     * Use the AMP player?
357     *
358     * There are multiple AMP plugins for WordPress, so multiple checks are performed.
359     *
360     * @since 3.0.7
361     *
362     * @return bool
363     */
364    public function useAmpPlayer()
365    {
366        // https://amp-wp.org/reference/function/amp_is_request/
367        if (function_exists('amp_is_request')) {
368            return \amp_is_request();
369        }
370
371        // https://ampforwp.com/tutorials/article/detect-amp-page-function/
372        if (function_exists('ampforwp_is_amp_endpoint')) {
373            return \ampforwp_is_amp_endpoint();
374        }
375
376        // https://amp-wp.org/reference/function/is_amp_endpoint/
377        if (function_exists('is_amp_endpoint')) {
378            return \is_amp_endpoint();
379        }
380
381        return false;
382    }
383
384    /**
385     * Use Player JS SDK?
386     *
387     * @since 3.0.7
388     *
389     * @return string
390     */
391    public function usePlayerJsSdk()
392    {
393        // AMP requests don't use the Player JS SDK
394        if ($this->useAmpPlayer()) {
395            return false;
396        }
397
398        // Both Gutenberg/Classic editors have their own player scripts
399        if (CoreUtils::isGutenbergPage() || CoreUtils::isEditScreen()) {
400            return false;
401        }
402
403        // Disable audio player in Preview, because we have not sent updates to BeyondWords API yet
404        if (function_exists('is_preview') && is_preview()) {
405            return false;
406        }
407
408        $post = get_post();
409
410        if (! $post) {
411            return false;
412        }
413
414        $projectId = PostMetaUtils::getProjectId($post->ID);
415        $contentId = PostMetaUtils::getContentId($post->ID);
416
417        if ($projectId && $contentId) {
418            return true;
419        }
420
421        return false;
422    }
423
424    /**
425     * JavaScript SDK parameters.
426     *
427     * @since 3.1.0
428     * @since 4.0.0 Use new JS SDK params format.
429     * @since 5.3.0 Prioritise post-specific player settings, falling-back to the
430     *              values of the "Player" tab in the plugin settings.
431     * @since 5.3.0 Support loadContentAs param and return an object.
432     *
433     * @param WP_Post $post WordPress Post.
434     *
435     * @return object
436     */
437    public function jsPlayerParams($post)
438    {
439        if (!($post instanceof \WP_Post)) {
440            return [];
441        }
442
443        $projectId = PostMetaUtils::getProjectId($post->ID);
444        $contentId = PostMetaUtils::getContentId($post->ID);
445
446        $params = [
447            'projectId' => is_numeric($projectId) ? (int)$projectId : $projectId,
448            'contentId' => is_numeric($contentId) ? (int)$contentId : $contentId,
449        ];
450
451        // Set initial SDK params from plugin settings
452        $params = $this->addPluginSettingsToSdkParams($params);
453
454        // Player UI
455        $playerUI = get_option('beyondwords_player_ui', PlayerUI::ENABLED);
456        if ($playerUI === PlayerUI::HEADLESS) {
457            $params['showUserInterface'] = false;
458        }
459
460        // Player Style
461        // @todo overwrite global styles with post settings
462        $playerStyle = PostMetaUtils::getPlayerStyle($post->ID);
463        if (!empty($playerStyle)) {
464            $params['playerStyle'] = $playerStyle;
465        }
466
467        // Player content
468        $playerContent = get_post_meta($post->ID, 'beyondwords_player_content', true);
469        if (!empty($playerContent)) {
470            $params['loadContentAs'] = [ $playerContent ];
471        }
472
473        /**
474         * Filters the BeyondWords JavaScript SDK parameters.
475         *
476         * @since 4.0.0
477         *
478         * @param array $params The default JS SDK params.
479         * @param int   $postId The Post ID.
480         */
481        $params = apply_filters('beyondwords_player_sdk_params', $params, $post->ID);
482
483        // Cast assoc array to object
484        return (object)$params;
485    }
486
487    /**
488     * Add plugin settings to SDK params.
489     *
490     * @since 5.0.0
491     *
492     * @param array $params BeyondWords Player SDK params.
493     *
494     * @return array Modified SDK params.
495     */
496    public function addPluginSettingsToSdkParams($params)
497    {
498        $mapping = [
499            'beyondwords_player_style'              => 'playerStyle',
500            'beyondwords_player_call_to_action'     => 'callToAction',
501            'beyondwords_player_highlight_sections' => 'highlightSections',
502            'beyondwords_player_widget_style'       => 'widgetStyle',
503            'beyondwords_player_widget_position'    => 'widgetPosition',
504            'beyondwords_player_skip_button_style'  => 'skipButtonStyle',
505        ];
506
507        foreach ($mapping as $wpOption => $sdkParam) {
508            $val = get_option($wpOption);
509            if (!empty($val)) {
510                $params[$sdkParam] = $val;
511            }
512        }
513
514        // Special case for clickableSections
515        $val = get_option('beyondwords_player_clickable_sections');
516        if (!empty($val)) {
517            $params['clickableSections'] = 'body';
518        }
519
520        return $params;
521    }
522}