PluginProbe
BeyondWords – AI audio for publishers / 6.3.0
BeyondWords – AI audio for publishers v6.3.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 / Component / Post / PostContentUtils.php

PostContentUtils.php in BeyondWords – AI audio for publishers 6.3.0, at src/Component/Post/PostContentUtils.php

403 lines 13.0 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\Component\Post;
6
7 /**
8 * BeyondWords Post Content Utilities.
9 *
10 * @package Beyondwords
11 * @subpackage Beyondwords/includes
12 * @author Stuart McAlpine <stu@beyondwords.io>
13 * @since 3.5.0
14 */
15 defined('ABSPATH') || exit;
16
17 class PostContentUtils
18 {
19 public const DATE_FORMAT = 'Y-m-d\TH:i:s\Z';
20
21 /**
22 * Get the content "body" param for the audio, ready to be sent to the
23 * BeyondWords API.
24 *
25 * From API version 1.1 the "summary" param is going to be used differently,
26 * so for WordPress we now prepend the WordPress excerpt to the "body" param.
27 *
28 * @param int|\WP_Post $post The WordPress post ID, or post object.
29 *
30 * @since 4.6.0
31 *
32 * @return string The content body param.
33 */
34 public static function getContentBody(int|\WP_Post $post): string|null
35 {
36 $post = get_post($post);
37
38 if (!($post instanceof \WP_Post)) {
39 throw new \Exception(esc_html__('Post Not Found', 'speechkit'));
40 }
41
42 $summary = PostContentUtils::getPostSummary($post);
43 $body = PostContentUtils::getPostBody($post);
44
45 if ($summary) {
46 $format = PostContentUtils::getPostSummaryWrapperFormat($post);
47
48 $body = sprintf($format, $summary) . $body;
49 }
50
51 return $body;
52 }
53
54 /**
55 * Get the post body for the audio content.
56 *
57 * @since 3.0.0
58 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
59 * @since 3.8.0 Exclude Gutenberg blocks with attribute { beyondwordsAudio: false }
60 * @since 4.0.0 Renamed from PostContentUtils::getSourceTextForAudio() to PostContentUtils::getBody()
61 * @since 4.6.0 Renamed from PostContentUtils::getBody() to PostContentUtils::getPostBody()
62 * @since 4.7.0 Remove wpautop filter for block editor API requests.
63 * @since 5.0.0 Remove SpeechKit-Start shortcode.
64 * @since 5.0.0 Remove beyondwords_content filter.
65 *
66 * @param int|\WP_Post $post The WordPress post ID, or post object.
67 *
68 * @return string The body (the processed $post->post_content).
69 */
70 public static function getPostBody(int|\WP_Post $post): string|null
71 {
72 $post = get_post($post);
73
74 if (!($post instanceof \WP_Post)) {
75 throw new \Exception(esc_html__('Post Not Found', 'speechkit'));
76 }
77
78 $content = PostContentUtils::getContentWithoutExcludedBlocks($post);
79
80 if (has_blocks($post)) {
81 // wpautop breaks our HTML markup when block editor paragraphs are empty
82 remove_filter('the_content', 'wpautop');
83
84 // But we still want to remove empty lines
85 $content = preg_replace('/^\h*\v+/m', '', $content);
86 }
87
88 // Apply the_content filters to handle shortcodes etc
89 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying core WordPress filter
90 $content = apply_filters('the_content', $content);
91
92 // Trim to remove trailing newlines – common for WordPress content
93 return trim($content);
94 }
95
96 /**
97 * Get the post summary wrapper format.
98 *
99 * This is a <div> with optional attributes depending on the BeyondWords
100 * data of the post.
101 *
102 * @param int|\WP_Post $post The WordPress post ID, or post object.
103 *
104 * @since 4.6.0
105 *
106 * @return string The summary wrapper <div>.
107 */
108 public static function getPostSummaryWrapperFormat(int|\WP_Post $post): string
109 {
110 $post = get_post($post);
111
112 if (!($post instanceof \WP_Post)) {
113 throw new \Exception(esc_html__('Post Not Found', 'speechkit'));
114 }
115
116 $summaryVoiceId = intval(get_post_meta($post->ID, 'beyondwords_summary_voice_id', true));
117
118 if ($summaryVoiceId > 0) {
119 return '<div data-beyondwords-summary="true" data-beyondwords-voice-id="' . $summaryVoiceId . '">%s</div>';
120 }
121
122 return '<div data-beyondwords-summary="true">%s</div>';
123 }
124
125 /**
126 * Get the post summary for the audio content.
127 *
128 * @param int|\WP_Post $post The WordPress post ID, or post object.
129 *
130 * @since 4.0.0
131 * @since 4.6.0 Renamed from PostContentUtils::getSummary() to PostContentUtils::getPostSummary()
132 *
133 * @return string The summary.
134 */
135 public static function getPostSummary(int|\WP_Post $post): string|null
136 {
137 $post = get_post($post);
138
139 if (!($post instanceof \WP_Post)) {
140 throw new \Exception(esc_html__('Post Not Found', 'speechkit'));
141 }
142
143 $summary = null;
144
145 // Optionally send the excerpt to the REST API, if the plugin setting has been checked
146 $prependExcerpt = get_option('beyondwords_prepend_excerpt');
147
148 if ($prependExcerpt && has_excerpt($post)) {
149 // Escape characters
150 $summary = htmlentities($post->post_excerpt, ENT_QUOTES | ENT_XHTML);
151 // Apply WordPress filters
152 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying core WordPress filter
153 $summary = apply_filters('get_the_excerpt', $summary);
154 // Convert line breaks into paragraphs
155 $summary = trim(wpautop($summary));
156 }
157
158 return $summary;
159 }
160
161 /**
162 * Get the post content without blocks which have been filtered.
163 *
164 * We have added buttons into the Gutenberg editor to optionally exclude selected
165 * blocks from the source text for audio.
166 *
167 * This method filters all blocks, removing any which have been excluded.
168 *
169 * @param int|\WP_Post $post The WordPress post ID, or post object.
170 *
171 * @since 3.8.0
172 * @since 4.0.0 Replace for loop with array_reduce
173 * @since 6.0.0 Remove beyondwordsMarker attribute from rendered blocks.
174 *
175 * @return string The post body without excluded blocks.
176 */
177 public static function getContentWithoutExcludedBlocks(int|\WP_Post $post): string
178 {
179 if (! has_blocks($post)) {
180 return trim($post->post_content);
181 }
182
183 $blocks = parse_blocks($post->post_content);
184 $output = '';
185
186 $blocks = PostContentUtils::getAudioEnabledBlocks($post);
187
188 foreach ($blocks as $block) {
189 $output .= render_block($block);
190 }
191
192 return $output;
193 }
194
195 /**
196 * Get audio-enabled blocks.
197 *
198 * @param int|\WP_Post $post The WordPress post ID, or post object.
199 *
200 * @since 4.0.0
201 * @since 5.0.0 Remove beyondwords_post_audio_enabled_blocks filter.
202 *
203 * @return array The blocks.
204 */
205 public static function getAudioEnabledBlocks(int|\WP_Post $post): array
206 {
207 $post = get_post($post);
208
209 if (! ($post instanceof \WP_Post)) {
210 return [];
211 }
212
213 if (! has_blocks($post)) {
214 return [];
215 }
216
217 $allBlocks = parse_blocks($post->post_content);
218
219 return array_filter($allBlocks, function ($block) {
220 $enabled = true;
221
222 if (is_array($block['attrs']) && isset($block['attrs']['beyondwordsAudio'])) {
223 $enabled = (bool) $block['attrs']['beyondwordsAudio'];
224 }
225
226 return $enabled;
227 });
228 }
229
230 /**
231 * Get the body param we pass to the API.
232 *
233 * @since 3.0.0 Introduced as getBodyJson.
234 * @since 3.3.0 Added metadata to aid custom playlist generation.
235 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils.
236 * @since 3.10.4 Rename `published_at` API param to `publish_date`.
237 * @since 4.0.0 Use new API params.
238 * @since 4.0.3 Ensure `image_url` is always a string.
239 * @since 4.3.0 Rename from getBodyJson to getContentParams.
240 * @since 4.6.0 Remove summary param & prepend body with summary.
241 * @since 5.0.0 Remove beyondwords_body_params filter.
242 * @since 6.0.0 Cast return value to string.
243 *
244 * @static
245 * @param int $postId WordPress Post ID.
246 *
247 * @return string JSON endoded params.
248 **/
249 public static function getContentParams(int $postId): array|string
250 {
251 $body = [
252 'type' => 'auto_segment',
253 'title' => get_the_title($postId),
254 'body' => PostContentUtils::getContentBody($postId),
255 'source_url' => get_the_permalink($postId),
256 'source_id' => strval($postId),
257 'author' => PostContentUtils::getAuthorName($postId),
258 'image_url' => strval(wp_get_original_image_url(get_post_thumbnail_id($postId))),
259 'metadata' => PostContentUtils::getMetadata($postId),
260 'publish_date' => get_post_time(PostContentUtils::DATE_FORMAT, true, $postId),
261 ];
262
263 $status = get_post_status($postId);
264
265 /*
266 * If the post status is draft/pending then we explicity send
267 * { published: false } to the BeyondWords API, to prevent the
268 * generated audio from being published in playlists.
269 *
270 * We also omit { publish_date } because get_post_time() returns `false`
271 * for posts which are "Pending Review".
272 */
273 if (in_array($status, ['draft', 'pending'])) {
274 $body['published'] = false;
275 unset($body['publish_date']);
276 } elseif (get_option('beyondwords_project_auto_publish_enabled')) {
277 $body['published'] = true;
278 }
279
280 $languageCode = get_post_meta($postId, 'beyondwords_language_code', true);
281
282 if ($languageCode) {
283 $body['language'] = $languageCode;
284 }
285
286 $bodyVoiceId = intval(get_post_meta($postId, 'beyondwords_body_voice_id', true));
287
288 if ($bodyVoiceId > 0) {
289 $body['body_voice_id'] = $bodyVoiceId;
290 }
291
292 $titleVoiceId = intval(get_post_meta($postId, 'beyondwords_title_voice_id', true));
293
294 if ($titleVoiceId > 0) {
295 $body['title_voice_id'] = $titleVoiceId;
296 }
297
298 $summaryVoiceId = intval(get_post_meta($postId, 'beyondwords_summary_voice_id', true));
299
300 if ($summaryVoiceId > 0) {
301 $body['summary_voice_id'] = $summaryVoiceId;
302 }
303
304 /**
305 * Filters the params we send to the BeyondWords API 'content' endpoint.
306 *
307 * @since 4.0.0 Introduced as beyondwords_body_params
308 * @since 4.3.0 Renamed from beyondwords_body_params to beyondwords_content_params
309 *
310 * @param array $body The params we send to the BeyondWords API.
311 * @param array $postId WordPress post ID.
312 */
313 $body = apply_filters('beyondwords_content_params', $body, $postId);
314
315 return (string) wp_json_encode($body);
316 }
317
318 /**
319 * Get the post metadata to send with BeyondWords API requests.
320 *
321 * The metadata key is defined by the BeyondWords API as "A custom object
322 * for storing meta information".
323 *
324 * The metadata values are used to create filters for playlists in the
325 * BeyondWords dashboard.
326 *
327 * We currently only include taxonomies by default, and the output of this
328 * method can be filtered using the `beyondwords_post_metadata` filter.
329 *
330 * @since 3.3.0
331 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils.
332 * @since 5.0.0 Remove beyondwords_post_metadata filter.
333 *
334 * @param int $postId Post ID.
335 *
336 * @return object The metadata object (empty if no metadata).
337 */
338 public static function getMetadata(int $postId): array|object
339 {
340 $metadata = new \stdClass();
341
342 $taxonomy = PostContentUtils::getAllTaxonomiesAndTerms($postId);
343
344 if (count((array)$taxonomy)) {
345 $metadata->taxonomy = $taxonomy;
346 }
347
348 return $metadata;
349 }
350
351 /**
352 * Get all taxonomies, and their selected terms, for a post.
353 *
354 * Returns an associative array of taxonomy names and terms.
355 *
356 * For example:
357 *
358 * array(
359 * "categories" => array("Category 1"),
360 * "post_tag" => array("Tag 1", "Tag 2", "Tag 3"),
361 * )
362 *
363 * @since 3.3.0
364 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
365 *
366 * @param int $postId Post ID.
367 *
368 * @return object The taxonomies object (empty if no taxonomies).
369 */
370 public static function getAllTaxonomiesAndTerms(int $postId): array|object
371 {
372 $postType = get_post_type($postId);
373
374 $postTypeTaxonomies = get_object_taxonomies($postType);
375
376 $taxonomies = new \stdClass();
377
378 foreach ($postTypeTaxonomies as $postTypeTaxonomy) {
379 $terms = get_the_terms($postId, $postTypeTaxonomy);
380
381 if (! empty($terms) && ! is_wp_error($terms)) {
382 $taxonomies->{(string)$postTypeTaxonomy} = wp_list_pluck($terms, 'name');
383 }
384 }
385
386 return $taxonomies;
387 }
388
389 /**
390 * Get author name for a post.
391 *
392 * @since 3.10.4
393 *
394 * @param int $postId Post ID.
395 */
396 public static function getAuthorName(int $postId): string
397 {
398 $authorId = get_post_field('post_author', $postId);
399
400 return get_the_author_meta('display_name', $authorId);
401 }
402 }
403