PluginProbe
BeyondWords – AI audio for publishers / 4.7.0
BeyondWords – AI audio for publishers v4.7.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 4.7.0, at src/Component/Post/PostMetaUtils.php

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