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

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

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