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

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