PluginProbe
BeyondWords – AI audio for publishers / 4.2.0
BeyondWords – AI audio for publishers v4.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 4.7.0 All 43 releases
speechkit / src / Core / Player / Player.php

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

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