PluginProbe
BeyondWords – AI audio for publishers / 4.4.0
BeyondWords – AI audio for publishers v4.4.0
7.2.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 All 44 releases
speechkit / src / Core / Player / Player.php

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

634 lines 19.1 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\PlayerUI\PlayerStyle;
10 use Beyondwords\Wordpress\Component\Settings\PlayerVersion\PlayerVersion;
11 use Beyondwords\Wordpress\Component\Settings\SettingsUtils;
12 use Beyondwords\Wordpress\Core\Environment;
13 use Beyondwords\Wordpress\Core\CoreUtils;
14 use Symfony\Component\DomCrawler\Crawler;
15
16 /**
17 * The "Latest" BeyondWords Player.
18 *
19 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
20 **/
21 class Player
22 {
23 /**
24 * Constructor
25 */
26 public function __construct()
27 {
28 // Actions
29 add_action('init', array($this, 'registerShortcodes'));
30 add_action('wp_enqueue_scripts', array($this, 'enqueueScripts'));
31
32 // Filters
33 add_filter('the_content', array($this, 'autoPrependPlayer'), 1000000);
34 add_filter('newsstand_the_content', array($this, 'autoPrependPlayer'));
35 }
36
37 /**
38 * Register shortcodes.
39 *
40 * @since 4.2.0
41 */
42 public function registerShortcodes()
43 {
44 add_shortcode('beyondwords_player', array($this, 'playerShortcode'));
45 }
46
47 /**
48 * HTML output for the BeyondWords player shortcode.
49 *
50 * @since 4.2.0
51 *
52 * @param array $atts Shortcode attributes.
53 *
54 * @return string
55 */
56 public function playerShortcode()
57 {
58 return $this->playerHtml();
59 }
60
61 /**
62 * Auto-prepends the BeyondWords player to WordPress content.
63 *
64 * @since 3.0.0
65 * @since 4.2.0 Renamed from addPlayerToContent to autoPrependPlayer.
66 * @since 4.2.0 Perform hasCustomPlayer() check here.
67 *
68 * @param string $content WordPress content.
69 *
70 * @return string
71 */
72 public function autoPrependPlayer($content)
73 {
74 if ($this->hasCustomPlayer($content)) {
75 return $content;
76 }
77
78 return $this->playerHtml() . $content;
79 }
80
81 /**
82 * Player HTML.
83 *
84 * Displays JS SDK variant of the BeyondWords audio player, for both
85 * AMP and non-AMP content.
86 *
87 * @SuppressWarnings(PHPMD.NPathComplexity)
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("unused")
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 *
298 * @return bool
299 **/
300 public function isPlayerEnabled($post = null)
301 {
302 $post = get_post($post);
303
304 if (! ($post instanceof \WP_Post)) {
305 return false;
306 }
307
308 // Assume we can show the player
309 $enabled = true;
310
311 // Has 'Display Player' been unchecked?
312 if (PostMetaUtils::getDisabled($post->ID)) {
313 $enabled = false;
314 }
315
316 // Is the player ui enabled in plugin settings?
317 if ($enabled) {
318 $enabled = get_option('beyondwords_player_ui', PlayerUI::ENABLED) === PlayerUI::ENABLED;
319 }
320
321 /**
322 * Filters the enabled/disabled (shown/hidden) status of the player for each post.
323 *
324 * Scheduled for removal in plugin version 5.0.0.
325 *
326 * @since 3.3.3
327 *
328 * @deprecated 4.3.0 Instead, return an empty string in the `beyondwords_player_html` filter to hide the player.
329 * Alternatively, you can set `published` to `false` in the BeyondWords dashboard.
330 *
331 * @param boolean $enabled Is the player enabled (shown) for this post?
332 * @param int $post_id WordPress post ID.
333 */
334 $enabled = apply_filters('beyondwords_post_player_enabled', $enabled, $post->ID);
335
336 return $enabled;
337 }
338
339 /**
340 * Register the JavaScript for the public-facing side of the site.
341 *
342 * @since 3.0.0
343 *
344 * @return void
345 */
346 public function enqueueScripts()
347 {
348 if (! is_singular()) {
349 return;
350 }
351
352 if (get_option('beyondwords_player_ui', PlayerUI::ENABLED) === PlayerUI::DISABLED) {
353 return;
354 }
355
356 // JS SDK Player inline script, filtered by $this->scriptLoaderTag()
357 add_filter('script_loader_tag', array($this, 'scriptLoaderTag'), 10, 3);
358
359 wp_enqueue_script(
360 'beyondwords-sdk',
361 Environment::getJsSdkUrl(),
362 array(),
363 null,
364 true
365 );
366 }
367
368 /**
369 * Use the AMP player?
370 *
371 * There are multiple AMP plugins for WordPress, so multiple checks are performed.
372 *
373 * @since 3.0.7
374 *
375 * @return bool
376 */
377 public function useAmpPlayer()
378 {
379 // https://amp-wp.org/reference/function/amp_is_request/
380 if (function_exists('amp_is_request')) {
381 return \amp_is_request();
382 }
383
384 // https://ampforwp.com/tutorials/article/detect-amp-page-function/
385 if (function_exists('ampforwp_is_amp_endpoint')) {
386 return \ampforwp_is_amp_endpoint();
387 }
388
389 // https://amp-wp.org/reference/function/is_amp_endpoint/
390 if (function_exists('is_amp_endpoint')) {
391 return \is_amp_endpoint();
392 }
393
394 return false;
395 }
396
397 /**
398 * Filters the HTML script tag of an enqueued script.
399 *
400 * @param string $tag The <script> tag for the enqueued script.
401 * @param string $handle The script's registered handle.
402 * @param string $src The script's source URL.
403 *
404 * @since 3.0.0
405 * @since 4.0.0 Updated Player SDK and added `beyondwords_player_script_onload` filter
406 *
407 * @see https://developer.wordpress.org/reference/hooks/script_loader_tag/
408 * @see https://stackoverflow.com/a/59594789
409 *
410 * @return string
411 */
412 public function scriptLoaderTag($tag, $handle, $src)
413 {
414 if ($handle === 'beyondwords-sdk') :
415 if (! $this->usePlayerJsSdk()) {
416 return '';
417 }
418
419 $post = get_post();
420 $params = $this->jsPlayerParams($post);
421 $playerUI = get_option('beyondwords_player_ui', PlayerUI::ENABLED);
422
423 $paramsJson = wp_json_encode($params, JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES);
424
425 if ($playerUI === PlayerUI::HEADLESS) {
426 // Headless instantiates a player without a target
427 $onload = 'new BeyondWords.Player(' . $paramsJson . ');';
428 } else {
429 // Standard mode instantiates player(s) with every div[data-beyondwords-player] as the target(s)
430 $onload = <<<EOD
431 document.querySelectorAll("div[data-beyondwords-player]").forEach(function(el) {
432 new BeyondWords.Player({
433 ...$paramsJson,
434 target: el
435 });
436 });
437 EOD;
438 }
439
440 // strip newlines to prevent "invalid character" errors
441 $onload = str_replace(array("\r", "\n"), '', $onload);
442
443 // limit whitespace to 1 space for legibility
444 $onload = preg_replace('/\s+/', ' ', $onload);
445
446 /**
447 * Filters the onload attribute of the BeyondWords Player script.
448 *
449 * Note that the strings should be in double quotes, because the output
450 * of this is run through esc_js() before it is output into the DOM.
451 *
452 * @link https://developer.wordpress.org/reference/functions/esc_js/
453 *
454 * Also note that to support multiple players on one page, the
455 * default script uses `document.querySelectorAll() to target all
456 * instances of `div[data-beyondwords-player]` in the HTML source.
457 * If this approach is removed then multiple occurrences of the
458 * BeyondWords player in one page may not work as expected.
459 *
460 * @link https://github.com/beyondwords-io/player/blob/main/doc/getting-started.md#how-to-configure-it
461 *
462 * @since 4.0.0
463 *
464 * @param string $script The string value of the onload script.
465 * @param array $params The SDK params for the current post, including
466 * `projectId` and `contentId`.
467 */
468 $onload = apply_filters('beyondwords_player_script_onload', $onload, $params);
469
470 ob_start();
471
472 if ($playerUI === PlayerUI::ENABLED || $playerUI === PlayerUI::HEADLESS) :
473 ?>
474 <script
475 data-beyondwords-sdk="true"
476 async
477 defer
478 src="<?php echo esc_url($src); ?>"
479 onload='<?php echo esc_js($onload); ?>'
480 ></script>
481 <?php
482 endif;
483
484 return ob_get_clean();
485 endif;
486
487 return $tag;
488 }
489
490 /**
491 * JavaScript SDK parameters.
492 *
493 * Note that the default return value for this method is an associative array, but
494 * the HTML output will be forced to an object due to `wp_json_encode($params, JSON_FORCE_OBJECT)`
495 * in `Player::scriptLoaderTag()`.
496 *
497 * @since 3.1.0
498 * @since 4.0.0 Use new JS SDK params format.
499 *
500 * @param WP_Post $post WordPress Post.
501 *
502 * @return array
503 */
504 public function jsPlayerParams($post)
505 {
506 if (!($post instanceof \WP_Post)) {
507 return [];
508 }
509
510 $projectId = PostMetaUtils::getProjectId($post->ID);
511 $contentId = PostMetaUtils::getContentId($post->ID);
512 $playerStyle = PostMetaUtils::getPlayerStyle($post->ID);
513
514 $params = [
515 'projectId' => is_numeric($projectId) ? (int)$projectId : $projectId,
516 'contentId' => is_numeric($contentId) ? (int)$contentId : $contentId,
517 'playerStyle' => $playerStyle,
518 ];
519
520 $playerUI = get_option('beyondwords_player_ui', PlayerUI::ENABLED);
521
522 if ($playerUI === PlayerUI::HEADLESS) {
523 $params['showUserInterface'] = false;
524 }
525
526 /**
527 * Use legacy JS SDK params if player version setting is "0": "Legacy"
528 */
529 if (SettingsUtils::useLegacyPlayer()) {
530 $params = $this->convertLatestToLegacyParams($params);
531 }
532
533 /**
534 * Filters the BeyondWords JavaScript SDK parameters.
535 *
536 * @since 4.0.0
537 *
538 * @param array $params The default JS SDK params.
539 * @param int $postId The Post ID.
540 */
541 $params = apply_filters('beyondwords_player_sdk_params', $params, $post->ID);
542
543 return $params;
544 }
545
546 /**
547 * Convert latest JS SDK params into legacy format.
548 *
549 * @since 4.0.0
550 *
551 * @see https://docs.beyondwords.io/docs/javascript-sdk-automatic-player
552 *
553 * @param array $latestParams Latest JS SDK params
554 *
555 * @return array Legacy JS SDK params
556 */
557 public function convertLatestToLegacyParams($latestParams)
558 {
559 $skBackend = Environment::getBackendUrl();
560 $skBackendApi = Environment::getApiUrl();
561
562 $legacyParams = [
563 'projectId' => $latestParams['projectId'],
564 'podcastId' => $latestParams['contentId'],
565 ];
566
567 if ($latestParams['playerStyle'] = 'large') {
568 $legacyParams['playerType'] = 'manual';
569 }
570
571 if (strlen($skBackend)) {
572 $legacyParams['skBackend'] = esc_url($skBackend);
573 }
574
575 if (is_admin()) {
576 $legacyParams['apiWriteKey'] = $latestParams['writeToken'];
577 $legacyParams['processingStatus'] = true;
578
579 if (strlen($skBackendApi)) {
580 $legacyParams['skBackendApi'] = esc_url($skBackendApi);
581 }
582 }
583
584 if (defined('BEYONDWORDS_DEBUG') && BEYONDWORDS_DEBUG) {
585 $legacyParams['debug'] = true;
586 }
587
588 return $legacyParams;
589 }
590
591 /**
592 * Use Player JS SDK?
593 *
594 * @since 3.0.7
595 *
596 * @return string
597 */
598 public function usePlayerJsSdk()
599 {
600 // AMP requests don't use the Player JS SDK
601 if ($this->useAmpPlayer()) {
602 return false;
603 }
604
605 // Both Gutenberg/Classic editors have their own player scripts
606 if (CoreUtils::isGutenbergPage() || CoreUtils::isEditScreen()) {
607 return false;
608 }
609
610 // Disable audio player in Preview, because we have not sent updates to BeyondWords API yet
611 if (function_exists('is_preview') && is_preview()) {
612 return false;
613 }
614
615 $post = get_post();
616
617 if (! $post) {
618 return false;
619 }
620
621 $projectId = PostMetaUtils::getProjectId($post->ID);
622 if (! $projectId) {
623 return false;
624 }
625
626 $contentId = PostMetaUtils::getContentId($post->ID);
627 if (! $contentId) {
628 return false;
629 }
630
631 return true;
632 }
633 }
634