PluginProbe
BeyondWords – AI audio for publishers / 4.0.0
BeyondWords – AI audio for publishers v4.0.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.php

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

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