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 / Core / Core.php

Core.php in BeyondWords – AI audio for publishers 4.7.0, at src/Core/Core.php

485 lines 14.4 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\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 * API Client.
18 *
19 * @since 3.0.0
20 */
21 private $apiClient;
22
23 /**
24 * Constructor.
25 *
26 * @since 3.0.0
27 * @since 3.7.1 Remove the "X BeyondWords errors found" notice after a reported slow MySQL query.
28 * @since 3.9.0 Add actions for deleting/trashing/restoring posts.
29 * @since 4.0.0 Moved side-effects into init() method.
30 */
31 public function __construct($apiClient)
32 {
33 $this->apiClient = $apiClient;
34 }
35
36 /**
37 * Init.
38 *
39 * @since 4.0.0
40 */
41 public function init()
42 {
43 // Actions
44 add_action('enqueue_block_editor_assets', array($this, 'enqueueBlockEditorAssets'), 1, 0);
45 add_action('init', array($this, 'loadPluginTextdomain'));
46 add_action('init', array($this, 'registerMeta'), 99, 3);
47
48 // Actions for adding/updating posts
49 add_action('wp_after_insert_post', array($this, 'onAddOrUpdatePost'), 99);
50
51 // Actions for deleting/trashing/restoring posts
52 add_action('before_delete_post', array($this, 'onTrashOrDeletePost'));
53 add_action('trashed_post', array($this, 'onTrashOrDeletePost'));
54 add_action('untrashed_post', array($this, 'onUntrashPost'), 10);
55
56 add_filter('is_protected_meta', array($this, 'isProtectedMeta'), 10, 2);
57 }
58
59 /**
60 * Should process post status?
61 *
62 * @since 3.5.0
63 * @since 3.7.0 Process audio for posts with 'pending' status
64 *
65 * @param string $status WordPress post status (e.g. 'pending', 'publish', 'private', 'future', etc).
66 *
67 * @return boolean
68 */
69 public function shouldProcessPostStatus($status)
70 {
71 $statuses = ['pending', 'publish', 'private', 'future'];
72
73 /**
74 * Filters the post statuses that we consider for audio processing.
75 *
76 * When a post is saved with any other post status we will not send
77 * any data to the BeyondWords API.
78 *
79 * The default values are "pending", "publish", "private" and "future".
80 *
81 * Scheduled for removal in plugin version 5.0.0.
82 *
83 * @since 3.3.3
84 * @since 3.7.0 Process audio for posts with 'pending' status
85 *
86 * @deprecated 4.3.0 Replaced with beyondwords_settings_post_statuses.
87 *
88 * @param string[] $statuses The post statuses that we consider for audio processing.
89 */
90 $statuses = apply_filters('beyondwords_post_statuses', $statuses);
91
92 /**
93 * Filters the post statuses that we consider for audio processing.
94 *
95 * When a post is saved with any other post status we will not send
96 * any data to the BeyondWords API.
97 *
98 * The default values are "pending", "publish", "private" and "future".
99 *
100 * @since 3.3.3 Introduced as beyondwords_post_statuses.
101 * @since 3.7.0 Process audio for posts with 'pending' status.
102 * @since 4.3.0 Renamed from beyondwords_post_statuses to beyondwords_settings_post_statuses.
103 *
104 * @param string[] $statuses The post statuses that we consider for audio processing.
105 */
106 $statuses = apply_filters('beyondwords_settings_post_statuses', $statuses);
107
108 // Only generate audio for certain post statuses
109 if (is_array($statuses) && in_array($status, $statuses)) {
110 return true;
111 }
112
113 return false;
114 }
115
116 /**
117 * Should generate audio for post?
118 *
119 * @since 3.5.0
120 * @since 3.10.0 remove wp_is_post_revision check.
121 *
122 * @param int $postId WordPress Post ID.
123 *
124 * @return boolean
125 */
126 public function shouldGenerateAudioForPost($postId)
127 {
128 // Bail if this is an autosave
129 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
130 return false;
131 }
132
133 $status = get_post_status($postId);
134
135 // Bail if the post status is invalid
136 if (! $this->shouldProcessPostStatus($status)) {
137 return false;
138 }
139
140 $generateAudio = PostMetaUtils::hasGenerateAudio($postId);
141
142 // Bail if 'Generate Audio' has not been selected
143 if (! $generateAudio) {
144 return false;
145 }
146
147 return true;
148 }
149
150 /**
151 * Generate audio for post.
152 *
153 * @since 3.0.0
154 * @since 3.2.0 Added speechkit_post_statuses filter
155 * @since 3.5.0 Refactored, adding $this->shouldGenerateAudioForPost()
156 *
157 * @param int $postId WordPress Post ID.
158 *
159 * @return array|false Response from API, or false if audio was not generated.
160 */
161 public function generateAudioForPost($postId)
162 {
163 // Perform checks to see if this post should be processed
164 if (! $this->shouldGenerateAudioForPost($postId)) {
165 return false;
166 }
167
168 $projectId = PostMetaUtils::getProjectId($postId);
169
170 // Bail if we cannot determine a Project ID
171 if (! $projectId) {
172 return false;
173 }
174
175 // Does this post already have audio?
176 $contentId = PostMetaUtils::getContentId($postId);
177
178 // Has autoregeneration for Post updates been disabled?
179 if ($contentId) {
180 if (defined('BEYONDWORDS_AUTOREGENERATE') && ! BEYONDWORDS_AUTOREGENERATE) {
181 return false;
182 }
183
184 $response = $this->apiClient->updateAudio($postId);
185 } else {
186 $response = $this->apiClient->createAudio($postId);
187 }
188
189 $this->processResponse($response, $projectId, $postId);
190
191 return $response;
192 }
193
194 /**
195 * Delete audio for post.
196 *
197 * @since 4.0.5
198 *
199 * @param int $postId WordPress Post ID.
200 *
201 * @return array|false Response from API, or false if audio was not generated.
202 */
203 public function deleteAudioForPost($postId)
204 {
205 return $this->apiClient->deleteAudio($postId);
206 }
207
208 /**
209 * Batch delete audio for posts.
210 *
211 * @since 4.1.0
212 *
213 * @param int[] $postIds Array of WordPress Post IDs.
214 *
215 * @return array|false Response from API, or false if audio was not generated.
216 */
217 public function batchDeleteAudioForPosts($postIds)
218 {
219 return $this->apiClient->batchDeleteAudio($postIds);
220 }
221
222 /**
223 * Process the response body of a BeyondWords REST API response.
224 *
225 * @since 3.0.0
226 * @since 3.7.0 Stop saving response.access_key, we don't currently use it.
227 * @since 4.0.0 Replace Podcast IDs with Content IDs
228 * @since 4.5.0 Save response.preview_token to support post scheduling.
229 */
230 public function processResponse($response, $projectId, $postId)
231 {
232 if (! is_array($response)) {
233 return $response;
234 }
235
236 if (array_key_exists('id', $response)) {
237 // Save Project ID
238 update_post_meta($postId, 'beyondwords_project_id', $projectId);
239
240 // Save Content ID
241 update_post_meta($postId, 'beyondwords_content_id', $response['id']);
242
243 // Temporarily save into Podcast ID field to support downgrades to < 4.0.0
244 update_post_meta($postId, 'beyondwords_podcast_id', $response['id']);
245
246 if (array_key_exists('preview_token', $response)) {
247 // Save Preview Key
248 update_post_meta($postId, 'beyondwords_preview_token', $response['preview_token']);
249 }
250 }
251
252 return $response;
253 }
254
255 /**
256 * Enqueue Core (built & minified) JS for Block Editor.
257 *
258 * @since 3.0.0
259 * @since 4.5.1 Disable plugin features if we don't have valid API settings.
260 */
261 public function enqueueBlockEditorAssets()
262 {
263 if (! SettingsUtils::hasApiSettings()) {
264 return;
265 }
266
267 if (CoreUtils::isGutenbergPage()) {
268 $postType = get_post_type();
269
270 $postTypes = SettingsUtils::getCompatiblePostTypes();
271
272 if (in_array($postType, $postTypes, true)) {
273 $assetFile = include BEYONDWORDS__PLUGIN_DIR . 'build/index.asset.php';
274
275 // Register the Block Editor JS
276 wp_enqueue_script(
277 'beyondwords-block-js',
278 BEYONDWORDS__PLUGIN_URI . 'build/index.js',
279 $assetFile['dependencies'],
280 $assetFile['version'],
281 true
282 );
283 }
284 }
285 }
286
287 /**
288 * Load plugin textdomain.
289 *
290 * @since 3.5.0
291 *
292 * @return void
293 */
294 public function loadPluginTextdomain()
295 {
296 load_plugin_textdomain('speechkit');
297 }
298
299 /**
300 * Register meta fields for REST API output.
301 *
302 * It is recommended to register meta keys for a specific combination
303 * of object type and object subtype.
304 *
305 * @since 2.5.0
306 * @since 3.9.0 Don't register speechkit_status - downgrades to plugin v2.x are no longer expected.
307 *
308 * @return void
309 **/
310 public function registerMeta()
311 {
312 $postTypes = SettingsUtils::getCompatiblePostTypes();
313
314 if (is_array($postTypes)) {
315 $keys = CoreUtils::getPostMetaKeys('all');
316
317 foreach ($postTypes as $postType) {
318 $options = array(
319 'show_in_rest' => true,
320 'single' => true,
321 'type' => 'string',
322 'default' => '',
323 'object_subtype' => $postType,
324 'prepare_callback' => 'sanitize_text_field',
325 'sanitize_callback' => 'sanitize_text_field',
326 'auth_callback' => function () {
327 return current_user_can('edit_posts');
328 },
329 );
330
331 foreach ($keys as $key) {
332 register_meta('post', $key, $options);
333 }
334 }
335 }
336 }
337
338 /**
339 * Make all of our custom fields private, so they don't appear in the
340 * "Custom Fields" panel, which can cause conflicts for the Block Editor.
341 *
342 * https://github.com/WordPress/gutenberg/issues/23078
343 *
344 * @since 4.0.0
345 */
346 public function isProtectedMeta($protected, $metaKey)
347 {
348 $keysToProtect = CoreUtils::getPostMetaKeys('all');
349
350 if (in_array($metaKey, $keysToProtect, true)) {
351 $protected = true;
352 }
353
354 return $protected;
355 }
356
357 /**
358 * WP Trash/Delete Post action.
359 *
360 * Fires before a post has been trashed or deleted.
361 *
362 * We want to send a DELETE HTTP request when a post is either trashed or deleted, so the
363 * audio no longer appears in playlists, or in the publishers BeyondWords dashboard.
364 *
365 * @since 3.9.0
366 *
367 * @param int $postId Post ID.
368 *
369 * @return bool
370 **/
371 public function onTrashOrDeletePost($postId)
372 {
373 // Bail if this post has no Project ID / Content ID
374 if (! PostMetaUtils::getProjectId($postId) || ! PostMetaUtils::getContentId($postId)) {
375 return false;
376 }
377
378 $response = $this->apiClient->deleteAudio($postId);
379
380 if (
381 ! is_array($response) ||
382 ! array_key_exists('deleted', $response) ||
383 ! $response['deleted'] === true
384 ) {
385 $errorMessage = __('Unable to delete audio from BeyondWords dashboard');
386
387 if (is_array($response) && array_key_exists('message', $response)) {
388 $errorMessage .= ': ' . $response['message'];
389 }
390
391 update_post_meta($postId, 'beyondwords_error_message', $errorMessage);
392
393 return false;
394 }
395
396 return $response;
397 }
398
399 /**
400 * WP Untrash ("Restore") Post action.
401 *
402 * Fires before a post is restored from the Trash.
403 *
404 * We want to send a PUT HTTP request when a post is Untrashed, to "undelete" it from the BeyondWords dashboard.
405 *
406 * @since 3.9.0
407 *
408 * @param int $postId Post ID.
409 * @param string $previousStatus The status of the post at the point where it was trashed.
410 *
411 * @return bool|Response
412 **/
413 public function onUntrashPost($postId)
414 {
415 // Bail if this post has no Project ID / Content ID
416 if (! PostMetaUtils::getProjectId($postId) || ! PostMetaUtils::getContentId($postId)) {
417 return false;
418 }
419
420 $response = $this->apiClient->updateAudio($postId);
421
422 if (
423 ! is_array($response) ||
424 ! array_key_exists('id', $response) ||
425 ! array_key_exists('deleted', $response) ||
426 ! $response['deleted'] === false
427 ) {
428 $errorMessage = __('Unable to restore audio to BeyondWords dashboard');
429
430 if (is_array($response) && array_key_exists('message', $response)) {
431 $errorMessage .= ': ' . $response['message'];
432 }
433
434 update_post_meta($postId, 'beyondwords_error_message', $errorMessage);
435
436 return false;
437 }
438
439 return $response;
440 }
441
442 /**
443 * WP Save Post action.
444 *
445 * Fires after a post, its terms and meta data has been saved.
446 *
447 * @since 3.0.0
448 * @since 3.2.0 Added beyondwords_post_statuses filter.
449 * @since 3.6.1 Improve $postBefore hash comparison.
450 * @since 3.9.0 Renamed method from wpAfterInsertPost to onAddOrUpdatePost.
451 * @since 4.0.0 Removed hash comparison.
452 * @since 4.4.0 Delete audio if beyondwords_delete_content custom field is set.
453 * @since 4.5.0 Remove unwanted debugging custom fields.
454 *
455 * @param int $postId Post ID.
456 *
457 * @return bool|Response
458 **/
459 public function onAddOrUpdatePost($postId)
460 {
461 // Has the "Remove" feature been used?
462 if (get_post_meta($postId, 'beyondwords_delete_content', true) === '1') {
463 // Make DELETE API request
464 $this->deleteAudioForPost($postId);
465
466 // Remove custom fields
467 PostMetaUtils::removeAllBeyondwordsMetadata($postId);
468
469 return false;
470 }
471
472 $status = get_post_status($postId);
473
474 // Bail if the post status is invalid
475 if (! $this->shouldProcessPostStatus($status)) {
476 return false;
477 }
478
479 // Generate Audio for the updated post
480 $this->generateAudioForPost($postId);
481
482 return true;
483 }
484 }
485