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

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

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