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