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

583 lines 17.4 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\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 * @deprecated Scheduled for removal in v5.0
20 *
21 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
22 **/
23 class Player
24 {
25 /**
26 * Constructor
27 */
28 public function __construct()
29 {
30 // Actions
31 add_action('wp_enqueue_scripts', array($this, 'enqueueScripts'));
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 * Use the AMP player?
319 *
320 * There are multiple AMP plugins for WordPress, so multiple checks are performed.
321 *
322 * @since 3.0.7
323 *
324 * @return bool
325 */
326 public function useAmpPlayer()
327 {
328 // https://amp-wp.org/reference/function/amp_is_request/
329 if (function_exists('amp_is_request')) {
330 return \amp_is_request();
331 }
332
333 // https://ampforwp.com/tutorials/article/detect-amp-page-function/
334 if (function_exists('ampforwp_is_amp_endpoint')) {
335 return \ampforwp_is_amp_endpoint();
336 }
337
338 // https://amp-wp.org/reference/function/is_amp_endpoint/
339 if (function_exists('is_amp_endpoint')) {
340 return \is_amp_endpoint();
341 }
342
343 return false;
344 }
345
346 /**
347 * Filters the HTML script tag of an enqueued script.
348 *
349 * @param string $tag The <script> tag for the enqueued script.
350 * @param string $handle The script's registered handle.
351 * @param string $src The script's source URL.
352 *
353 * @since 3.0.0
354 * @since 4.0.0 Updated Player SDK and added `beyondwords_player_script_onload` filter
355 *
356 * @see https://developer.wordpress.org/reference/hooks/script_loader_tag/
357 * @see https://stackoverflow.com/a/59594789
358 *
359 * @return string
360 */
361 public function scriptLoaderTag($tag, $handle, $src)
362 {
363 if ($handle === 'beyondwords-sdk') :
364 if (! $this->usePlayerJsSdk()) {
365 return '';
366 }
367
368 $post = get_post();
369 $params = $this->jsPlayerParams($post);
370 $playerUI = get_option('beyondwords_player_ui', PlayerUI::ENABLED);
371
372 $paramsJson = wp_json_encode($params, JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES);
373
374 if ($playerUI === PlayerUI::HEADLESS) {
375 // Headless instantiates a player without a target
376 $onload = 'new BeyondWords.Player(' . $paramsJson . ');';
377 } else {
378 // Standard mode instantiates player(s) with every div[data-beyondwords-player] as the target(s)
379 $onload = <<<EOD
380 document.querySelectorAll("div[data-beyondwords-player]").forEach(function(el) {
381 new BeyondWords.Player({
382 ...$paramsJson,
383 target: el
384 });
385 });
386 EOD;
387 }
388
389 // strip newlines to prevent "invalid character" errors
390 $onload = str_replace(array("\r", "\n"), '', $onload);
391
392 // limit whitespace to 1 space for legibility
393 $onload = preg_replace('/\s+/', ' ', $onload);
394
395 /**
396 * Filters the onload attribute of the BeyondWords Player script.
397 *
398 * Note that the strings should be in double quotes, because the output
399 * of this is run through esc_js() before it is output into the DOM.
400 *
401 * @link https://developer.wordpress.org/reference/functions/esc_js/
402 *
403 * Also note that to support multiple players on one page, the
404 * default script uses `document.querySelectorAll() to target all
405 * instances of `div[data-beyondwords-player]` in the HTML source.
406 * If this approach is removed then multiple occurrences of the
407 * BeyondWords player in one page may not work as expected.
408 *
409 * @link https://github.com/beyondwords-io/player/blob/main/doc/getting-started.md#how-to-configure-it
410 *
411 * @since 4.0.0
412 *
413 * @param string $script The string value of the onload script.
414 * @param array $params The SDK params for the current post, including
415 * `projectId` and `contentId`.
416 */
417 $onload = apply_filters('beyondwords_player_script_onload', $onload, $params);
418
419 ob_start();
420
421 if ($playerUI === PlayerUI::ENABLED || $playerUI === PlayerUI::HEADLESS) :
422 ?>
423 <script
424 data-beyondwords-sdk="true"
425 async
426 defer
427 src="<?php echo esc_url($src); ?>"
428 onload='<?php echo esc_js($onload); ?>'
429 ></script>
430 <?php
431 endif;
432
433 return ob_get_clean();
434 endif;
435
436 return $tag;
437 }
438
439 /**
440 * JavaScript SDK parameters.
441 *
442 * Note that the default return value for this method is an associative array, but
443 * the HTML output will be forced to an object due to `wp_json_encode($params, JSON_FORCE_OBJECT)`
444 * in `Player::scriptLoaderTag()`.
445 *
446 * @since 3.1.0
447 * @since 4.0.0 Use new JS SDK params format.
448 *
449 * @param WP_Post $post WordPress Post.
450 *
451 * @return array
452 */
453 public function jsPlayerParams($post)
454 {
455 if (!($post instanceof \WP_Post)) {
456 return [];
457 }
458
459 $projectId = PostMetaUtils::getProjectId($post->ID);
460 $contentId = PostMetaUtils::getContentId($post->ID);
461 $playerStyle = PostMetaUtils::getPlayerStyle($post->ID);
462
463 $params = [
464 'projectId' => is_numeric($projectId) ? (int)$projectId : $projectId,
465 'contentId' => is_numeric($contentId) ? (int)$contentId : $contentId,
466 'playerStyle' => $playerStyle,
467 ];
468
469 $playerUI = get_option('beyondwords_player_ui', PlayerUI::ENABLED);
470
471 if ($playerUI === PlayerUI::HEADLESS) {
472 $params['showUserInterface'] = false;
473 }
474
475 /**
476 * Use legacy JS SDK params if player version setting is "0": "Legacy"
477 */
478 if (SettingsUtils::useLegacyPlayer()) {
479 $params = $this->convertLatestToLegacyParams($params);
480 }
481
482 /**
483 * Filters the BeyondWords JavaScript SDK parameters.
484 *
485 * @since 4.0.0
486 *
487 * @param array $params The default JS SDK params.
488 * @param int $postId The Post ID.
489 */
490 $params = apply_filters('beyondwords_player_sdk_params', $params, $post->ID);
491
492 return $params;
493 }
494
495 /**
496 * Convert latest JS SDK params into legacy format.
497 *
498 * @since 4.0.0
499 *
500 * @see https://docs.beyondwords.io/docs/javascript-sdk-automatic-player
501 *
502 * @param array $latestParams Latest JS SDK params
503 *
504 * @return array Legacy JS SDK params
505 */
506 public function convertLatestToLegacyParams($latestParams)
507 {
508 $skBackend = Environment::getBackendUrl();
509 $skBackendApi = Environment::getApiUrl();
510
511 $legacyParams = [
512 'projectId' => $latestParams['projectId'],
513 'podcastId' => $latestParams['contentId'],
514 ];
515
516 if ($latestParams['playerStyle'] = 'large') {
517 $legacyParams['playerType'] = 'manual';
518 }
519
520 if (strlen($skBackend)) {
521 $legacyParams['skBackend'] = esc_url($skBackend);
522 }
523
524 if (is_admin()) {
525 $legacyParams['apiWriteKey'] = $latestParams['writeToken'];
526 $legacyParams['processingStatus'] = true;
527
528 if (strlen($skBackendApi)) {
529 $legacyParams['skBackendApi'] = esc_url($skBackendApi);
530 }
531 }
532
533 if (defined('BEYONDWORDS_DEBUG') && BEYONDWORDS_DEBUG) {
534 $legacyParams['debug'] = true;
535 }
536
537 return $legacyParams;
538 }
539
540 /**
541 * Use Player JS SDK?
542 *
543 * @since 3.0.7
544 *
545 * @return string
546 */
547 public function usePlayerJsSdk()
548 {
549 // AMP requests don't use the Player JS SDK
550 if ($this->useAmpPlayer()) {
551 return false;
552 }
553
554 // Both Gutenberg/Classic editors have their own player scripts
555 if (CoreUtils::isGutenbergPage() || CoreUtils::isEditScreen()) {
556 return false;
557 }
558
559 // Disable audio player in Preview, because we have not sent updates to BeyondWords API yet
560 if (function_exists('is_preview') && is_preview()) {
561 return false;
562 }
563
564 $post = get_post();
565
566 if (! $post) {
567 return false;
568 }
569
570 $projectId = PostMetaUtils::getProjectId($post->ID);
571 if (! $projectId) {
572 return false;
573 }
574
575 $contentId = PostMetaUtils::getContentId($post->ID);
576 if (! $contentId) {
577 return false;
578 }
579
580 return true;
581 }
582 }
583