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 / PostMetaUtils.php

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

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