PluginProbe
BeyondWords – AI audio for publishers / 7.0.0
BeyondWords – AI audio for publishers v7.0.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 / post / class-meta.php

class-meta.php in BeyondWords – AI audio for publishers 7.0.0, at src/post/class-meta.php

408 lines 12.1 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\Post;
6
7 /**
8 * BeyondWords Post Meta (Custom Field) Utilities.
9 *
10 * @package Beyondwords
11 * @subpackage Beyondwords/includes
12 * @author Stuart McAlpine <stu@beyondwords.io>
13 * @since 3.5.0
14 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
15 */
16 defined( 'ABSPATH' ) || exit;
17
18 class Meta {
19
20 public const WP_ERROR_FORMAT = 'WP_Error [%s] %s';
21
22 /**
23 * Get "renamed" Post Meta.
24 *
25 * Checks `beyondwords_*` then the legacy `speechkit_*` prefix, migrating
26 * legacy values to the new prefix on read.
27 *
28 * @since 3.7.0
29 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
30 *
31 * @param int $post_id Post ID.
32 * @param string $name Custom field name, without the prefix.
33 *
34 * @return string
35 */
36 public static function get_renamed_post_meta( int $post_id, string $name ): mixed {
37 if ( metadata_exists( 'post', $post_id, 'beyondwords_' . $name ) ) {
38 return get_post_meta( $post_id, 'beyondwords_' . $name, true );
39 }
40
41 if ( metadata_exists( 'post', $post_id, 'speechkit_' . $name ) ) {
42 $value = get_post_meta( $post_id, 'speechkit_' . $name, true );
43
44 update_post_meta( $post_id, 'beyondwords_' . $name, $value );
45
46 return $value;
47 }
48
49 return '';
50 }
51
52 /**
53 * Get the BeyondWords metadata for a Post.
54 *
55 * @since 4.1.0 Append 'beyondwords_version' and 'wordpress_version'.
56 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
57 */
58 public static function get_all_beyondwords_metadata( int $post_id ): array {
59 global $wp_version;
60
61 $keys_to_check = \BeyondWords\Core\Utils::get_post_meta_keys( 'all' );
62
63 $metadata = has_meta( $post_id );
64
65 $metadata = array_filter( $metadata, fn( $item ) => in_array( $item['meta_key'], $keys_to_check ) );
66
67 // phpcs:disable WordPress.DB.SlowDBQuery
68 array_push(
69 $metadata,
70 [
71 'meta_id' => null,
72 'meta_key' => 'beyondwords_version',
73 'meta_value' => BEYONDWORDS__PLUGIN_VERSION,
74 ],
75 [
76 'meta_id' => null,
77 'meta_key' => 'wordpress_version',
78 'meta_value' => $wp_version,
79 ],
80 [
81 'meta_id' => null,
82 'meta_key' => 'wordpress_post_id',
83 'meta_value' => $post_id,
84 ],
85 );
86 // phpcs:enable WordPress.DB.SlowDBQuery
87
88 return $metadata;
89 }
90
91 /**
92 * Remove the BeyondWords metadata for a Post.
93 *
94 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
95 *
96 * @param int $post_id Post ID.
97 *
98 * @since 4.x Introduced.
99 * @since 6.0.1 Use \BeyondWords\Core\Utils::get_post_meta_keys() to get all keys.
100 */
101 public static function remove_all_beyondwords_metadata( int $post_id ): void {
102 $keys = \BeyondWords\Core\Utils::get_post_meta_keys( 'all' );
103
104 foreach ( $keys as $key ) {
105 delete_post_meta( $post_id, $key, null );
106 }
107 }
108
109 /**
110 * Check if a Post should have BeyondWords content (a Content entity in BeyondWords).
111 *
112 * @since 6.0.0 Introduced.
113 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
114 *
115 * @param int $post_id Post ID.
116 *
117 * @return bool True if the post should have BeyondWords content, false otherwise.
118 */
119 public static function has_content( int $post_id ): bool {
120 $content_id = self::get_content_id( $post_id );
121 $integration_method = get_post_meta( $post_id, 'beyondwords_integration_method', true );
122
123 // Unset means REST API, for legacy compatibility.
124 if ( empty( $integration_method ) ) {
125 $integration_method = \BeyondWords\Settings\Fields::INTEGRATION_REST_API;
126 }
127
128 if ( \BeyondWords\Settings\Fields::INTEGRATION_REST_API === $integration_method && ! empty( $content_id ) ) {
129 return true;
130 }
131
132 // Strict: don't fall back to the plugin-setting project ID.
133 $project_id = self::get_project_id( $post_id, true );
134
135 if ( \BeyondWords\Settings\Fields::INTEGRATION_CLIENT_SIDE === $integration_method && ! empty( $project_id ) ) {
136 return true;
137 }
138
139 return false;
140 }
141
142 /**
143 * Get the Content ID for a WordPress Post.
144 *
145 * Over time there have been various approaches to storing the Content ID.
146 * This function tries each approach in reverse-date order.
147 *
148 * @since 3.0.0
149 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
150 * @since 4.0.0 Renamed to getContentId() & prioritise beyondwords_content_id
151 * @since 5.0.0 Remove beyondwords_content_id filter.
152 * @since 6.0.0 Add fallback parameter to allow falling back to Post ID.
153 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
154 *
155 * @param int $post_id Post ID.
156 * @param bool $fallback If true, will fall back to the Post ID if no Content ID is found.
157 *
158 * @return string|false Content ID, or false
159 */
160 public static function get_content_id( int $post_id, bool $fallback = false ): string|int|false {
161 $content_id = get_post_meta( $post_id, 'beyondwords_content_id', true );
162 if ( ! empty( $content_id ) ) {
163 return $content_id;
164 }
165
166 $podcast_id = self::get_podcast_id( $post_id );
167 if ( ! empty( $podcast_id ) ) {
168 return $podcast_id;
169 }
170
171 if ( $fallback ) {
172 return (string) $post_id;
173 }
174
175 return false;
176 }
177
178 /**
179 * Sanitize a BeyondWords Content ID (numeric ID or UUID).
180 *
181 * Anything beyond alphanumerics and hyphens is rejected: the ID is later
182 * interpolated into API URL paths, where `/`, `?`, `#` etc. would allow
183 * path/query injection against the authenticated API.
184 *
185 * @since 7.0.0
186 *
187 * @param mixed $content_id Raw submitted Content ID.
188 *
189 * @return string Sanitized Content ID, or '' when the value is invalid.
190 */
191 public static function sanitize_content_id( $content_id ): string {
192 $content_id = sanitize_text_field( (string) $content_id );
193
194 return preg_match( '/^[a-zA-Z0-9-]*$/', $content_id ) ? $content_id : '';
195 }
196
197 /**
198 * Get the (legacy) Podcast ID for a WordPress Post.
199 *
200 * Over time there have been various approaches to storing the Podcast ID.
201 * This function tries each approach in reverse-date order.
202 *
203 * @since 3.0.0
204 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
205 * @since 4.0.0 Allow string values for UUIDs stored >= v4.x
206 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
207 *
208 * @param int $post_id Post ID.
209 *
210 * @return int|false Podcast ID, or false
211 */
212 public static function get_podcast_id( int $post_id ): string|int|false {
213 $podcast_id = self::get_renamed_post_meta( $post_id, 'podcast_id' );
214
215 if ( $podcast_id ) {
216 return $podcast_id;
217 }
218
219 // Legacy player URLs are /a/[ID], /e/[ID] or /m/[ID].
220 $speechkit_link = get_post_meta( $post_id, '_speechkit_link', true );
221 preg_match( '/\/[aem]\/(\d+)/', (string) $speechkit_link, $matches );
222 if ( $matches ) {
223 return intval( $matches[1] );
224 }
225
226 $speechkit_response = self::get_http_response_body_from_post_meta( $post_id, 'speechkit_response' );
227 preg_match( '/"podcast_id":(")?(\d+)(?(1)\1|)/', (string) $speechkit_response, $matches );
228 if ( $matches && $matches[2] ) {
229 return intval( $matches[2] );
230 }
231
232 // Mirrors the if/else at legacy Speechkit_Public::iframe_player_embed_html();
233 // only the share_url branch produces a usable Podcast ID for us.
234 $article = get_post_meta( $post_id, 'speechkit_info', true );
235 if ( ! empty( $article ) && isset( $article['share_url'] ) ) {
236 preg_match( '/\/[aem]\/(\d+)/', (string) $article['share_url'], $matches );
237 if ( $matches ) {
238 return intval( $matches[1] );
239 }
240 }
241
242 return false;
243 }
244
245 /**
246 * Get the BeyondWords preview token for a WordPress Post.
247 *
248 * The token allows previewing audio in admin before its scheduled publish date.
249 *
250 * @since 4.5.0
251 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
252 *
253 * @param int $post_id Post ID.
254 *
255 * @return string Preview token
256 */
257 public static function get_preview_token( int $post_id ): string|false {
258 $preview_token = get_post_meta( $post_id, 'beyondwords_preview_token', true );
259
260 return $preview_token ?: false;
261 }
262
263 /**
264 * Get the 'Generate Audio' value for a Post.
265 *
266 * @since 3.0.0
267 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
268 * @since 6.0.0 Add Magic Embed support.
269 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
270 *
271 * @param int $post_id Post ID.
272 */
273 public static function has_generate_audio( int $post_id ): bool {
274 $generate_audio = self::get_renamed_post_meta( $post_id, 'generate_audio' );
275
276 if ( $generate_audio === '1' ) {
277 return true;
278 }
279
280 if ( $generate_audio === '0' ) {
281 return false;
282 }
283
284 return \BeyondWords\Editor\Components\GenerateAudio::should_preselect_generate_audio( $post_id );
285 }
286
287 /**
288 * Get the Project ID for a WordPress Post.
289 *
290 * The plugin-setting project ID may have changed since a post was created,
291 * so historic custom fields are checked before falling back to the setting.
292 *
293 * @since 3.0.0
294 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
295 * @since 4.0.0 Apply beyondwords_project_id filter
296 * @since 5.0.0 Remove beyondwords_project_id filter.
297 * @since 6.0.0 Support Magic Embed and add strict mode.
298 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
299 *
300 * @param int $post_id Post ID.
301 * @param bool $strict Strict mode, which only checks the custom field. Defaults to false.
302 *
303 * @return int|false Project ID, or false
304 */
305 public static function get_project_id( int $post_id, bool $strict = false ): int|string|false {
306 if ( $strict ) {
307 return self::get_renamed_post_meta( $post_id, 'project_id' );
308 }
309
310 $post_meta = intval( self::get_renamed_post_meta( $post_id, 'project_id' ) );
311
312 if ( ! empty( $post_meta ) ) {
313 return $post_meta;
314 }
315
316 $speechkit_response = self::get_http_response_body_from_post_meta( $post_id, 'speechkit_response' );
317
318 preg_match( '/"project_id":(")?(\d+)(?(1)\1|)/', (string) $speechkit_response, $matches );
319
320 if ( $matches && $matches[2] ) {
321 return intval( $matches[2] );
322 }
323
324 $setting = get_option( 'beyondwords_project_id' );
325
326 if ( $setting ) {
327 return intval( $setting );
328 }
329
330 return false;
331 }
332
333 /**
334 * Get the Body Voice ID for a WordPress Post.
335 *
336 * We do not filter this, because the Block Editor directly accesses this
337 * custom field, bypassing any filters we add here.
338 *
339 * @since 4.0.0
340 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
341 *
342 * @param int $post_id Post ID.
343 *
344 * @return int|false Body Voice ID, or false
345 */
346 public static function get_body_voice_id( int $post_id ): int|string|false {
347 $voice_id = get_post_meta( $post_id, 'beyondwords_body_voice_id', true );
348
349 return $voice_id ?: false;
350 }
351
352 /**
353 * Get the "Error Message" value for a WordPress Post.
354 *
355 * @since 3.7.0
356 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
357 *
358 * @param int $post_id Post ID.
359 *
360 * @return string
361 */
362 public static function get_error_message( int $post_id ): string|false {
363 return self::get_renamed_post_meta( $post_id, 'error_message' );
364 }
365
366 /**
367 * Get the "Disabled" value for a WordPress Post.
368 *
369 * @since 3.7.0
370 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
371 *
372 * @param int $post_id Post ID.
373 */
374 public static function get_disabled( int $post_id ): bool {
375 return (bool) self::get_renamed_post_meta( $post_id, 'disabled' );
376 }
377
378 /**
379 * Get HTTP response body from post meta.
380 *
381 * Legacy rows may hold a whole WordPress HTTP response array or a WP_Error;
382 * both are normalised to a string.
383 *
384 * @since 3.0.3
385 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils
386 * @since 3.6.1 Handle responses saved as object of class WP_Error
387 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
388 *
389 * @param int $post_id Post ID.
390 * @param string $meta_name Post Meta name.
391 *
392 * @return string
393 */
394 public static function get_http_response_body_from_post_meta( int $post_id, string $meta_name ): array|string|false {
395 $post_meta = get_post_meta( $post_id, $meta_name, true );
396
397 if ( is_array( $post_meta ) ) {
398 return (string) wp_remote_retrieve_body( $post_meta );
399 }
400
401 if ( is_wp_error( $post_meta ) ) {
402 return sprintf( self::WP_ERROR_FORMAT, $post_meta->get_error_code(), $post_meta->get_error_message() );
403 }
404
405 return (string) $post_meta;
406 }
407 }
408