PluginProbe
BeyondWords – AI audio for publishers / 6.0.3
BeyondWords – AI audio for publishers v6.0.3
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.0.3, at src/Component/Post/PostContentUtils.php

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