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