PluginProbe
BeyondWords – AI audio for publishers / 4.6.0
BeyondWords – AI audio for publishers v4.6.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 / Component / Post / PostMetaUtils.php

PostMetaUtils.php in BeyondWords – AI audio for publishers 4.6.0, at src/Component/Post/PostMetaUtils.php

506 lines 15.6 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 use Beyondwords\Wordpress\Component\Settings\PlayerStyle\PlayerStyle;
8 use Beyondwords\Wordpress\Core\CoreUtils;
9
10 /**
11 * BeyondWords Post Meta (Custom Field) Utilities.
12 *
13 * @package Beyondwords
14 * @subpackage Beyondwords/includes
15 * @author Stuart McAlpine <[email protected]>
16 * @since 3.5.0
17 */
18 class PostMetaUtils
19 {
20 public const WP_ERROR_FORMAT = 'WP_Error [%s] %s';
21
22 /**
23 * Get "renamed" Post Meta.
24 *
25 * We previously saved custom fields with a prefix of `speechkit_*` and we now
26 * save them with a prefix of `beyondwords_*`.
27 *
28 * This method checks both prefixes, copying `speechkit_*` data to `beyondwords_*`.
29 *
30 * @since 3.7.0
31 *
32 * @param int $postId Post ID.
33 * @param string $name Custom field name, without the prefix.
34 *
35 * @return string
36 */
37 public static function getRenamedPostMeta($postId, $name)
38 {
39 if (metadata_exists('post', $postId, 'beyondwords_' . $name)) {
40 return get_post_meta($postId, 'beyondwords_' . $name, true);
41 }
42
43 if (metadata_exists('post', $postId, 'speechkit_' . $name)) {
44 $value = get_post_meta($postId, 'speechkit_' . $name, true);
45
46 // Migrate over to newer `beyondwords_*` format
47 update_post_meta($postId, 'beyondwords_' . $name, $value);
48
49 return $value;
50 }
51
52 return '';
53 }
54
55 /**
56 * Get the BeyondWords metadata for a Post.
57 *
58 * @since 4.1.0 Append 'beyondwords_version' and 'wordpress_version'.
59 */
60 public static function getAllBeyondwordsMetadata($postId)
61 {
62 global $wp_version;
63
64 $keysToCheck = CoreUtils::getPostMetaKeys('all');
65
66 $metadata = has_meta($postId);
67
68 $metadata = array_filter($metadata, function ($item) use ($keysToCheck) {
69 return in_array($item['meta_key'], $keysToCheck);
70 });
71
72 // Prepend the WordPress Post ID to the meta data
73 array_push(
74 $metadata,
75 [
76 'meta_id' => null,
77 'meta_key' => 'beyondwords_version',
78 'meta_value' => BEYONDWORDS__PLUGIN_VERSION, // phpcs:ignore
79 ],
80 [
81 'meta_id' => null,
82 'meta_key' => 'wordpress_version',
83 'meta_value' => $wp_version, // phpcs:ignore
84 ],
85 [
86 'meta_id' => null,
87 'meta_key' => 'wordpress_post_id',
88 'meta_value' => $postId, // phpcs:ignore
89 ],
90 );
91
92 return $metadata;
93 }
94
95 /**
96 * Remove the BeyondWords metadata for a Post.
97 */
98 public static function removeAllBeyondwordsMetadata($postId)
99 {
100 $keysToCheck = [
101 'beyondwords_generate_audio',
102 'beyondwords_project_id',
103 'beyondwords_content_id',
104 'beyondwords_podcast_id',
105 'beyondwords_preview_token',
106 'beyondwords_player_style',
107 'beyondwords_language_id',
108 'beyondwords_body_voice_id',
109 'beyondwords_title_voice_id',
110 'beyondwords_summary_voice_id',
111 'beyondwords_error_message',
112 'beyondwords_disabled',
113 'beyondwords_delete_content',
114 'publish_post_to_speechkit',
115 'speechkit_generate_audio',
116 'speechkit_project_id',
117 'speechkit_podcast_id',
118 'speechkit_error_message',
119 'speechkit_disabled',
120 'speechkit_access_key',
121 'speechkit_error',
122 'speechkit_info',
123 'speechkit_response',
124 'speechkit_retries',
125 'speechkit_status',
126 '_speechkit_link',
127 '_speechkit_text',
128 ];
129
130 foreach ($keysToCheck as $key) {
131 delete_post_meta($postId, $key, null);
132 }
133
134 return true;
135 }
136
137 /**
138 * Get the Content ID for a WordPress Post.
139 *
140 * Over time there have been various approaches to storing the Content ID.
141 * This function tries each approach in reverse-date order.
142 *
143 * @since 3.0.0
144 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
145 * @since 4.0.0 Renamed to getContentId() & prioritise beyondwords_content_id
146 *
147 * @param int $postId Post ID.
148 *
149 * @return int|false Content ID, or false
150 */
151 public static function getContentId($postId)
152 {
153 // Check for "Content ID" custom field (string, uuid)
154 $contentId = get_post_meta($postId, 'beyondwords_content_id', true);
155
156 if (! $contentId) {
157 // Also try "Podcast ID" custom field (number, or string for > 4.x)
158 $contentId = PostMetaUtils::getPodcastId($postId);
159 }
160
161 /**
162 * Filters the BeyondWords Content ID.
163 *
164 * Scheduled for removal in plugin version 5.0.0.
165 *
166 * @since 4.0.0
167 *
168 * @deprecated 4.3.0 No replacement is expected. Email [email protected] if you are using this filter.
169 *
170 * @param string $contentId The BeyondWords Content ID.
171 * @param int $postId The post ID.
172 */
173 $contentId = apply_filters('beyondwords_content_id', $contentId, $postId);
174
175 return $contentId;
176 }
177
178 /**
179 * Get the (legacy) Podcast ID for a WordPress Post.
180 *
181 * Over time there have been various approaches to storing the Podcast ID.
182 * This function tries each approach in reverse-date order.
183 *
184 * @since 3.0.0
185 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
186 * @since 4.0.0 Allow string values for UUIDs stored >= v4.x
187 *
188 * @param int $postId Post ID.
189 *
190 * @return int|false Podcast ID, or false
191 */
192 public static function getPodcastId($postId)
193 {
194 // Check for "Podcast ID" custom field (number, or string for > 4.x)
195 $podcastId = PostMetaUtils::getRenamedPostMeta($postId, 'podcast_id');
196
197 if ($podcastId) {
198 return $podcastId;
199 }
200
201 // It may also be found by parsing post_meta._speechkit_link
202 $speechkit_link = get_post_meta($postId, '_speechkit_link', true);
203 // Player URL can be either /a/[ID] or /e/[ID] or /m/[ID]
204 preg_match('/\/[aem]\/(\d+)/', (string)$speechkit_link, $matches);
205 if ($matches) {
206 return intval($matches[1]);
207 }
208
209 // It may also be found by parsing post_meta.speechkit_response
210 $speechkit_response = static::getHttpResponseBodyFromPostMeta($postId, 'speechkit_response');
211 preg_match('/"podcast_id":(")?(\d+)(?(1)\1|)/', (string)$speechkit_response, $matches);
212 // $matches[2] is the Podcast ID (response.podcast_id)
213 if ($matches && $matches[2]) {
214 return intval($matches[2]);
215 }
216
217 /**
218 * It may also be found by parsing post_meta.speechkit_info
219 *
220 * NOTE: This has been copied verbatim from the existing iframe player check
221 * at Speechkit_Public::iframe_player_embed_html(), in case it is
222 * needed for posts which were created a very long time ago.
223 * I cannot write unit tests for this to pass, they always fail for me,
224 * so there are currently no tests for it.
225 **/
226 $article = get_post_meta($postId, 'speechkit_info', true);
227 if (empty($article) || ! isset($article['share_url'])) {
228 // This is exactly the same if/else statement that we have at
229 // Speechkit_Public::iframe_player_embed_html(), but there is
230 // nothing for us to to do here.
231 } else {
232 // This is the part that we need...
233 $url = $article['share_url'];
234
235 // Player URL can be either /a/[ID] or /e/[ID] or /m/[ID]
236 preg_match('/\/[aem]\/(\d+)/', (string)$url, $matches);
237 if ($matches) {
238 return intval($matches[1]);
239 }
240 }
241
242 // todo throw ContentIdNotFoundException???
243
244 return false;
245 }
246
247 /**
248 * Get the BeyondWords preview token for a WordPress Post.
249 *
250 * The preview token allows us to play audio that has a future scheduled
251 * publish date, so we can preview the audio in WordPress admin before it
252 * is published.
253 *
254 * The token is supplied by the BeyondWords REST API whenever audio content
255 * is created/updated, and stored in a WordPress custom field.
256 *
257 * @since 4.5.0
258 *
259 * @param int $postId Post ID.
260 *
261 * @return string Preview token
262 */
263 public static function getPreviewToken($postId)
264 {
265 $previewToken = get_post_meta($postId, 'beyondwords_preview_token', true);
266
267 return $previewToken;
268 }
269
270 /**
271 * Get the 'Generate Audio' value for a Post.
272 *
273 * @since 3.0.0
274 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
275 *
276 * @param int $postId Post ID.
277 *
278 * @return bool
279 */
280 public static function hasGenerateAudio($postId)
281 {
282 if (PostMetaUtils::getRenamedPostMeta($postId, 'generate_audio') === '1') {
283 return true;
284 }
285
286 if (get_post_meta($postId, 'publish_post_to_speechkit', true) === '1') {
287 return true;
288 }
289
290 $projectId = PostMetaUtils::getProjectId($postId);
291 $contentId = PostMetaUtils::getContentId($postId);
292
293 if ($projectId && $contentId) {
294 return true;
295 }
296
297 return false;
298 }
299
300 /**
301 * Get the Project ID for a WordPress Post.
302 *
303 * It is possible to change the BeyondWords project ID in the plugin settings,
304 * so the current Project ID setting will not necessarily be correct for all
305 * historic Posts. Because of this, we attempt to retrive the correct Project ID
306 * from various custom fields, then fall-back to the plugin setting.
307 *
308 * @since 3.0.0
309 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
310 * @since 4.0.0 Apply beyondwords_project_id filter
311 *
312 * @param int $postId Post ID.
313 *
314 * @return int|false Project ID, or false
315 */
316 public static function getProjectId($postId)
317 {
318 // Check custom fields
319 $projectId = intval(PostMetaUtils::getRenamedPostMeta($postId, 'project_id'));
320
321 if (! $projectId) {
322 // It may also be found by parsing post_meta.speechkit_response
323 $speechkit_response = static::getHttpResponseBodyFromPostMeta($postId, 'speechkit_response');
324 preg_match('/"project_id":(")?(\d+)(?(1)\1|)/', (string)$speechkit_response, $matches);
325 // $matches[2] is the Project ID (response.project_id)
326 if ($matches && $matches[2]) {
327 $projectId = intval($matches[2]);
328 }
329 }
330
331 // If we still don't have an ID then use the default from the plugin settings
332 if (! $projectId) {
333 $projectId = intval(get_option('beyondwords_project_id'));
334 }
335
336 if (! $projectId) {
337 // Return boolean false instead of numeric 0
338 $projectId = false;
339 }
340
341 /**
342 * Filters the BeyondWords Project ID.
343 *
344 * Scheduled for removal in plugin version 5.0.0.
345 *
346 * @since 4.0.0
347 *
348 * @deprecated 4.3.0 No replacement is expected. Email [email protected] if you are using this filter.
349 *
350 * @param string $contentId The BeyondWords Project ID.
351 * @param int $postId The post ID.
352 */
353 $projectId = apply_filters('beyondwords_project_id', $projectId, $postId);
354
355 // todo throw ProjectIdNotFoundException?
356
357 return $projectId;
358 }
359
360 /**
361 * Get the Body Voice ID for a WordPress Post.
362 *
363 * We do not filter this, because the Block Editor directly accesses this
364 * custom field, bypassing any filters we add here.
365 *
366 * @since 4.0.0
367 *
368 * @param int $postId Post ID.
369 *
370 * @return int|false Body Voice ID, or false
371 */
372 public static function getBodyVoiceId($postId)
373 {
374 $voiceId = get_post_meta($postId, 'beyondwords_body_voice_id', true);
375
376 return $voiceId;
377 }
378
379 /**
380 * Get the Title Voice ID for a WordPress Post.
381 *
382 * We do not filter this, because the Block Editor directly accesses this
383 * custom field, bypassing any filters we add here.
384 *
385 * @since 4.0.0
386 *
387 * @param int $postId Post ID.
388 *
389 * @return int|false Title Voice ID, or false
390 */
391 public static function getTitleVoiceId($postId)
392 {
393 $voiceId = get_post_meta($postId, 'beyondwords_title_voice_id', true);
394
395 return $voiceId;
396 }
397
398 /**
399 * Get the Summary Voice ID for a WordPress Post.
400 *
401 * We do not filter this, because the Block Editor directly accesses this
402 * custom field, bypassing any filters we add here.
403 *
404 * @since 4.0.0
405 *
406 * @param int $postId Post ID.
407 *
408 * @return int|false Summary Voice ID, or false
409 */
410 public static function getSummaryVoiceId($postId)
411 {
412 $voiceId = get_post_meta($postId, 'beyondwords_summary_voice_id', true);
413
414 return $voiceId;
415 }
416
417 /**
418 * Get the player style for a post.
419 *
420 * Defaults to the plugin setting if the custom field doesn't exist.
421 *
422 * @since 4.1.0
423 *
424 * @param int $postId Post ID.
425 *
426 * @return string Player style.
427 */
428 public static function getPlayerStyle($postId)
429 {
430 $playerStyle = get_post_meta($postId, 'beyondwords_player_style', true);
431
432 // Prefer custom field
433 if ($playerStyle) {
434 return $playerStyle;
435 }
436
437 // Fall back to plugin setting
438 return get_option('beyondwords_player_style', PlayerStyle::STANDARD);
439 }
440
441 /**
442 * Get the "Error Message" value for a WordPress Post.
443 *
444 * Supports data saved with the `beyondwords_*` prefix and the legacy `speechkit_*` prefix.
445 *
446 * @since 3.7.0
447 *
448 * @param int $postId Post ID.
449 *
450 * @return string
451 */
452 public static function getErrorMessage($postId)
453 {
454 return PostMetaUtils::getRenamedPostMeta($postId, 'error_message');
455 }
456
457 /**
458 * Get the "Disabled" value for a WordPress Post.
459 *
460 * Supports data saved with the `beyondwords_*` prefix and the legacy `speechkit_*` prefix.
461 *
462 * @since 3.7.0
463 *
464 * @param int $postId Post ID.
465 *
466 * @return string
467 */
468 public static function getDisabled($postId)
469 {
470 return PostMetaUtils::getRenamedPostMeta($postId, 'disabled');
471 }
472
473 /**
474 * Get HTTP response body from post meta.
475 *
476 * The data may have been saved as a WordPress HTTP response array. If it was,
477 * then return the 'body' key of the HTTP response instead of the raw post meta.
478 *
479 * The data may also have been saved as a WordPress WP_Error instance. If it was,
480 * then return a string containing the WP_Error code and message.
481 *
482 * @since 3.0.3
483 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
484 * @since 3.6.1 Handle responses saved as object of class WP_Error
485 *
486 * @param int $postId Post ID.
487 * @param string $metaName Post Meta name.
488 *
489 * @return string
490 */
491 public static function getHttpResponseBodyFromPostMeta($postId, $metaName)
492 {
493 $postMeta = get_post_meta($postId, $metaName, true);
494
495 if (is_array($postMeta)) {
496 return (string)wp_remote_retrieve_body($postMeta);
497 }
498
499 if (is_wp_error($postMeta)) {
500 return sprintf(PostMetaUtils::WP_ERROR_FORMAT, $postMeta->get_error_code(), $postMeta->get_error_message());
501 }
502
503 return (string)$postMeta;
504 }
505 }
506