Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.85% |
94 / 107 |
|
46.67% |
7 / 15 |
CRAP | |
0.00% |
0 / 1 |
Core | |
87.85% |
94 / 107 |
|
46.67% |
7 / 15 |
50.96 | |
0.00% |
0 / 1 |
init | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
shouldProcessPostStatus | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
shouldGenerateAudioForPost | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
7.02 | |||
generateAudioForPost | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
5.02 | |||
deleteAudioForPost | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
batchDeleteAudioForPosts | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
processResponse | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
9 | |||
enqueueBlockEditorAssets | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
loadPluginTextdomain | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
registerMeta | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
4.00 | |||
isProtectedMeta | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onTrashPost | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
onDeletePost | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onAddOrUpdatePost | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
2.50 | |||
getLangCodeFromJsonIfEmpty | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Beyondwords\Wordpress\Core; |
6 | |
7 | use Beyondwords\Wordpress\Component\Post\PostMetaUtils; |
8 | use Beyondwords\Wordpress\Component\Settings\SettingsUtils; |
9 | use Beyondwords\Wordpress\Core\CoreUtils; |
10 | |
11 | /** |
12 | * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
13 | **/ |
14 | class Core |
15 | { |
16 | /** |
17 | * Init. |
18 | * |
19 | * @since 4.0.0 |
20 | */ |
21 | public function init() |
22 | { |
23 | // Actions |
24 | add_action('enqueue_block_editor_assets', array($this, 'enqueueBlockEditorAssets'), 1, 0); |
25 | add_action('init', array($this, 'loadPluginTextdomain')); |
26 | add_action('init', array($this, 'registerMeta'), 99, 3); |
27 | |
28 | // Actions for adding/updating posts |
29 | add_action('wp_after_insert_post', array($this, 'onAddOrUpdatePost'), 99); |
30 | |
31 | // Actions for trashing/deleting posts |
32 | add_action('wp_trash_post', array($this, 'onTrashPost')); |
33 | add_action('before_delete_post', array($this, 'onDeletePost')); |
34 | |
35 | add_filter('is_protected_meta', array($this, 'isProtectedMeta'), 10, 2); |
36 | |
37 | // Older posts may be missing beyondwords_language_code, so we'll try to set it. |
38 | add_filter('get_post_metadata', array($this, 'getLangCodeFromJsonIfEmpty'), 10, 3); |
39 | } |
40 | |
41 | /** |
42 | * Should process post status? |
43 | * |
44 | * @since 3.5.0 |
45 | * @since 3.7.0 Process audio for posts with 'pending' status |
46 | * @since 5.0.0 Remove beyondwords_post_statuses filter. |
47 | * |
48 | * @param string $status WordPress post status (e.g. 'pending', 'publish', 'private', 'future', etc). |
49 | * |
50 | * @return boolean |
51 | */ |
52 | public function shouldProcessPostStatus($status) |
53 | { |
54 | $statuses = ['pending', 'publish', 'private', 'future']; |
55 | |
56 | /** |
57 | * Filters the post statuses that we consider for audio processing. |
58 | * |
59 | * When a post is saved with any other post status we will not send |
60 | * any data to the BeyondWords API. |
61 | * |
62 | * The default values are "pending", "publish", "private" and "future". |
63 | * |
64 | * @since 3.3.3 Introduced as beyondwords_post_statuses. |
65 | * @since 3.7.0 Process audio for posts with 'pending' status. |
66 | * @since 4.3.0 Renamed from beyondwords_post_statuses to beyondwords_settings_post_statuses. |
67 | * |
68 | * @param string[] $statuses The post statuses that we consider for audio processing. |
69 | */ |
70 | $statuses = apply_filters('beyondwords_settings_post_statuses', $statuses); |
71 | |
72 | // Only generate audio for certain post statuses |
73 | if (is_array($statuses) && in_array($status, $statuses)) { |
74 | return true; |
75 | } |
76 | |
77 | return false; |
78 | } |
79 | |
80 | /** |
81 | * Should generate audio for post? |
82 | * |
83 | * @since 3.5.0 |
84 | * @since 3.10.0 Remove wp_is_post_revision check |
85 | * @since 5.1.0 Regenerate audio for all post statuses |
86 | * |
87 | * @param int $postId WordPress Post ID. |
88 | * |
89 | * @return boolean |
90 | */ |
91 | public function shouldGenerateAudioForPost($postId) |
92 | { |
93 | // Autosaves don't generate audio |
94 | if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { |
95 | return false; |
96 | } |
97 | |
98 | // A project ID is required |
99 | $projectId = PostMetaUtils::getProjectId($postId); |
100 | if (! $projectId) { |
101 | return false; |
102 | } |
103 | |
104 | // Regenerate if post has audio (regardless of post status) |
105 | $contentId = PostMetaUtils::getContentId($postId); |
106 | if ($contentId) { |
107 | return true; |
108 | } |
109 | |
110 | $generateAudio = PostMetaUtils::hasGenerateAudio($postId); |
111 | $status = get_post_status($postId); |
112 | |
113 | // Generate if audio has been requested for a valid post status |
114 | if ($generateAudio && $this->shouldProcessPostStatus($status)) { |
115 | return true; |
116 | } |
117 | |
118 | // Default is no audio |
119 | return false; |
120 | } |
121 | |
122 | /** |
123 | * Generate audio for a post if certain conditions are met. |
124 | * |
125 | * @since 3.0.0 |
126 | * @since 3.2.0 Added speechkit_post_statuses filter |
127 | * @since 3.5.0 Refactored, adding $this->shouldGenerateAudioForPost() |
128 | * @since 5.1.0 Move project ID check into $this->shouldGenerateAudioForPost() |
129 | * |
130 | * @param int $postId WordPress Post ID. |
131 | * |
132 | * @return array|false Response from API, or false if audio was not generated. |
133 | */ |
134 | public function generateAudioForPost($postId) |
135 | { |
136 | // Perform checks to see if this post should be processed |
137 | if (! $this->shouldGenerateAudioForPost($postId)) { |
138 | return false; |
139 | } |
140 | |
141 | // Does this post already have audio? |
142 | $contentId = PostMetaUtils::getContentId($postId); |
143 | |
144 | // Has autoregeneration for Post updates been disabled? |
145 | if ($contentId) { |
146 | if (defined('BEYONDWORDS_AUTOREGENERATE') && ! BEYONDWORDS_AUTOREGENERATE) { |
147 | return false; |
148 | } |
149 | |
150 | $response = ApiClient::updateAudio($postId); |
151 | } else { |
152 | $response = ApiClient::createAudio($postId); |
153 | } |
154 | |
155 | $projectId = PostMetaUtils::getProjectId($postId); |
156 | |
157 | $this->processResponse($response, $projectId, $postId); |
158 | |
159 | return $response; |
160 | } |
161 | |
162 | /** |
163 | * Delete audio for post. |
164 | * |
165 | * @since 4.0.5 |
166 | * |
167 | * @param int $postId WordPress Post ID. |
168 | * |
169 | * @return array|false Response from API, or false if audio was not generated. |
170 | */ |
171 | public function deleteAudioForPost($postId) |
172 | { |
173 | return ApiClient::deleteAudio($postId); |
174 | } |
175 | |
176 | /** |
177 | * Batch delete audio for posts. |
178 | * |
179 | * @since 4.1.0 |
180 | * |
181 | * @param int[] $postIds Array of WordPress Post IDs. |
182 | * |
183 | * @return array|false Response from API, or false if audio was not generated. |
184 | */ |
185 | public function batchDeleteAudioForPosts($postIds) |
186 | { |
187 | return ApiClient::batchDeleteAudio($postIds); |
188 | } |
189 | |
190 | /** |
191 | * Process the response body of a BeyondWords REST API response. |
192 | * |
193 | * @since 3.0.0 |
194 | * @since 3.7.0 Stop saving response.access_key, we don't currently use it. |
195 | * @since 4.0.0 Replace Podcast IDs with Content IDs |
196 | * @since 4.5.0 Save response.preview_token to support post scheduling. |
197 | * @since 5.0.0 Stop saving `beyondwords_podcast_id`. |
198 | */ |
199 | public function processResponse($response, $projectId, $postId) |
200 | { |
201 | if (! is_array($response)) { |
202 | return $response; |
203 | } |
204 | |
205 | if ($projectId && ! empty($response['id'])) { |
206 | update_post_meta($postId, 'beyondwords_project_id', $projectId); |
207 | update_post_meta($postId, 'beyondwords_content_id', $response['id']); |
208 | |
209 | if (! empty($response['preview_token'])) { |
210 | update_post_meta($postId, 'beyondwords_preview_token', $response['preview_token']); |
211 | } |
212 | |
213 | if (! empty($response['language'])) { |
214 | update_post_meta($postId, 'beyondwords_language_code', $response['language']); |
215 | } |
216 | |
217 | if (! empty($response['title_voice_id'])) { |
218 | update_post_meta($postId, 'beyondwords_title_voice_id', $response['title_voice_id']); |
219 | } |
220 | |
221 | if (! empty($response['summary_voice_id'])) { |
222 | update_post_meta($postId, 'beyondwords_summary_voice_id', $response['summary_voice_id']); |
223 | } |
224 | |
225 | if (! empty($response['body_voice_id'])) { |
226 | update_post_meta($postId, 'beyondwords_body_voice_id', $response['body_voice_id']); |
227 | } |
228 | } |
229 | |
230 | return $response; |
231 | } |
232 | |
233 | /** |
234 | * Enqueue Core (built & minified) JS for Block Editor. |
235 | * |
236 | * @since 3.0.0 |
237 | * @since 4.5.1 Disable plugin features if we don't have valid API settings. |
238 | */ |
239 | public function enqueueBlockEditorAssets() |
240 | { |
241 | if (! SettingsUtils::hasValidApiConnection()) { |
242 | return; |
243 | } |
244 | |
245 | $postType = get_post_type(); |
246 | |
247 | $postTypes = SettingsUtils::getCompatiblePostTypes(); |
248 | |
249 | if (in_array($postType, $postTypes, true)) { |
250 | $assetFile = include BEYONDWORDS__PLUGIN_DIR . 'build/index.asset.php'; |
251 | |
252 | // Register the Block Editor JS |
253 | wp_enqueue_script( |
254 | 'beyondwords-block-js', |
255 | BEYONDWORDS__PLUGIN_URI . 'build/index.js', |
256 | $assetFile['dependencies'], |
257 | $assetFile['version'], |
258 | true |
259 | ); |
260 | } |
261 | } |
262 | |
263 | /** |
264 | * Load plugin textdomain. |
265 | * |
266 | * @since 3.5.0 |
267 | * |
268 | * @return void |
269 | */ |
270 | public function loadPluginTextdomain() |
271 | { |
272 | load_plugin_textdomain('speechkit'); |
273 | } |
274 | |
275 | /** |
276 | * Register meta fields for REST API output. |
277 | * |
278 | * It is recommended to register meta keys for a specific combination |
279 | * of object type and object subtype. |
280 | * |
281 | * @since 2.5.0 |
282 | * @since 3.9.0 Don't register speechkit_status - downgrades to plugin v2.x are no longer expected. |
283 | * |
284 | * @return void |
285 | **/ |
286 | public function registerMeta() |
287 | { |
288 | $postTypes = SettingsUtils::getCompatiblePostTypes(); |
289 | |
290 | if (is_array($postTypes)) { |
291 | $keys = CoreUtils::getPostMetaKeys('all'); |
292 | |
293 | foreach ($postTypes as $postType) { |
294 | $options = array( |
295 | 'show_in_rest' => true, |
296 | 'single' => true, |
297 | 'type' => 'string', |
298 | 'default' => '', |
299 | 'object_subtype' => $postType, |
300 | 'prepare_callback' => 'sanitize_text_field', |
301 | 'sanitize_callback' => 'sanitize_text_field', |
302 | 'auth_callback' => function () { |
303 | return current_user_can('edit_posts'); |
304 | }, |
305 | ); |
306 | |
307 | foreach ($keys as $key) { |
308 | register_meta('post', $key, $options); |
309 | } |
310 | } |
311 | } |
312 | } |
313 | |
314 | /** |
315 | * Make all of our custom fields private, so they don't appear in the |
316 | * "Custom Fields" panel, which can cause conflicts for the Block Editor. |
317 | * |
318 | * https://github.com/WordPress/gutenberg/issues/23078 |
319 | * |
320 | * @since 4.0.0 |
321 | */ |
322 | public function isProtectedMeta($protected, $metaKey) |
323 | { |
324 | $keysToProtect = CoreUtils::getPostMetaKeys('all'); |
325 | |
326 | if (in_array($metaKey, $keysToProtect, true)) { |
327 | $protected = true; |
328 | } |
329 | |
330 | return $protected; |
331 | } |
332 | |
333 | /** |
334 | * On trash post. |
335 | * |
336 | * We attempt to send a DELETE REST API request when a post is trashed so the audio |
337 | * no longer appears in playlists, or in the publishers BeyondWords dashboard. |
338 | * |
339 | * @since 3.9.0 Introduced. |
340 | * @since 5.4.0 Renamed from onTrashOrDeletePost, and we now remove all |
341 | * BeyondWords data when a post is trashed. |
342 | * |
343 | * @param int $postId Post ID. |
344 | * |
345 | * @return bool |
346 | **/ |
347 | public function onTrashPost($postId) |
348 | { |
349 | ApiClient::deleteAudio($postId); |
350 | PostMetaUtils::removeAllBeyondwordsMetadata($postId); |
351 | } |
352 | |
353 | /** |
354 | * On delete post. |
355 | * |
356 | * We attempt to send a DELETE REST API request when a post is deleted so the audio |
357 | * no longer appears in playlists, or in the publishers BeyondWords dashboard. |
358 | * |
359 | * @since 5.4.0 Introduced, replacing onTrashOrDeletePost. |
360 | * |
361 | * @param int $postId Post ID. |
362 | * |
363 | * @return bool |
364 | **/ |
365 | public function onDeletePost($postId) |
366 | { |
367 | ApiClient::deleteAudio($postId); |
368 | } |
369 | |
370 | /** |
371 | * WP Save Post action. |
372 | * |
373 | * Fires after a post, its terms and meta data has been saved. |
374 | * |
375 | * @since 3.0.0 |
376 | * @since 3.2.0 Added beyondwords_post_statuses filter. |
377 | * @since 3.6.1 Improve $postBefore hash comparison. |
378 | * @since 3.9.0 Renamed method from wpAfterInsertPost to onAddOrUpdatePost. |
379 | * @since 4.0.0 Removed hash comparison. |
380 | * @since 4.4.0 Delete audio if beyondwords_delete_content custom field is set. |
381 | * @since 4.5.0 Remove unwanted debugging custom fields. |
382 | * @since 5.1.0 Move post status check out of here |
383 | * |
384 | * @param int $postId Post ID. |
385 | * |
386 | * @return bool|Response |
387 | **/ |
388 | public function onAddOrUpdatePost($postId) |
389 | { |
390 | // Has the "Remove" feature been used? |
391 | if (get_post_meta($postId, 'beyondwords_delete_content', true) === '1') { |
392 | // Make DELETE API request |
393 | $this->deleteAudioForPost($postId); |
394 | |
395 | // Remove custom fields |
396 | PostMetaUtils::removeAllBeyondwordsMetadata($postId); |
397 | |
398 | return false; |
399 | } |
400 | |
401 | $this->generateAudioForPost($postId); |
402 | |
403 | return true; |
404 | } |
405 | |
406 | /** |
407 | * Get the language code from a JSON mapping if it is empty. |
408 | * |
409 | * @since 5.4.0 Introduced. |
410 | * |
411 | * @param mixed $value The value of the metadata. |
412 | * @param int $object_id The ID of the object metadata is for. |
413 | * @param string $meta_key The key of the metadata. |
414 | * @param bool $single Whether to return a single value. |
415 | * |
416 | * @return mixed |
417 | */ |
418 | public function getLangCodeFromJsonIfEmpty($value, $object_id, $meta_key) |
419 | { |
420 | if ('beyondwords_language_code' === $meta_key && empty($value)) { |
421 | $languageId = get_post_meta($object_id, 'beyondwords_language_id', true); |
422 | |
423 | if ($languageId) { |
424 | $langCodes = json_decode(file_get_contents(BEYONDWORDS__PLUGIN_DIR . 'assets/lang-codes.json'), true); |
425 | |
426 | if (is_array($langCodes) && array_key_exists($languageId, $langCodes)) { |
427 | return [$langCodes[$languageId]]; |
428 | } |
429 | } |
430 | } |
431 | |
432 | return $value; |
433 | } |
434 | } |