PluginProbe
BeyondWords – AI audio for publishers / 4.7.0
BeyondWords – AI audio for publishers v4.7.0
7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.7.0 All 43 releases
speechkit / src / Core / Player / Player.php

Player.php in BeyondWords – AI audio for publishers 4.7.0, at src/Core/Player/Player.php

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