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

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