PluginProbe
BeyondWords – AI audio for publishers / 4.6.0
BeyondWords – AI audio for publishers v4.6.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.6.0, at src/Core/Player/Player.php

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