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

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