PluginProbe
BeyondWords – AI audio for publishers / 4.4.0
BeyondWords – AI audio for publishers v4.4.0
7.2.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 All 44 releases
speechkit / src / Core / ApiClient.php

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

636 lines 17.3 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\Core\Environment;
8 use Beyondwords\Wordpress\Core\Request;
9 use Beyondwords\Wordpress\Component\Post\PostContentUtils;
10 use Beyondwords\Wordpress\Component\Post\PostMetaUtils;
11 use Beyondwords\Wordpress\Component\Settings\SettingsUtils;
12
13 /**
14 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
15 **/
16 class ApiClient
17 {
18 public const ERROR_FORMAT = '#%s: %s';
19
20 public $errors;
21
22 /**
23 * Constructor
24 *
25 * @since 3.0.0
26 */
27 public function __construct()
28 {
29 add_action('admin_notices', array($this, 'adminNotices'));
30
31 $this->errors = [];
32 }
33
34 /**
35 * POST /projects/:id/content.
36 *
37 * @since 3.0.0
38 *
39 * @param $postId WordPress Post ID
40 *
41 * @return Response|false Response, or false
42 **/
43 public function createAudio($postId)
44 {
45 $projectId = PostMetaUtils::getProjectId($postId);
46
47 if (! $projectId) {
48 return false;
49 }
50
51 $url = sprintf('%s/projects/%d/content', Environment::getApiUrl(), $projectId);
52
53 $body = PostContentUtils::getContentParams($postId);
54
55 $request = new Request('POST', $url, $body);
56
57 return $this->callApi($postId, $request);
58 }
59
60 /**
61 * PUT /projects/:id/content/:id.
62 *
63 * @since 3.0.0
64 *
65 * @param $postId WordPress Post ID
66 *
67 * @return Response|false Response, or false
68 **/
69 public function updateAudio($postId)
70 {
71 $projectId = PostMetaUtils::getProjectId($postId);
72 $contentId = PostMetaUtils::getContentId($postId);
73
74 if (! $projectId || ! $contentId) {
75 return false;
76 }
77
78 $url = sprintf('%s/projects/%d/content/%s', Environment::getApiUrl(), $projectId, $contentId);
79
80 $body = PostContentUtils::getContentParams($postId);
81
82 $request = new Request('PUT', $url, $body);
83
84 return $this->callApi($postId, $request);
85 }
86
87 /**
88 * DELETE /projects/:id/content/:id.
89 *
90 * @since 3.0.0
91 *
92 * @param int $postId WordPress Post ID
93 *
94 * @return Response|false Response, or false
95 **/
96 public function deleteAudio($postId)
97 {
98 $projectId = PostMetaUtils::getProjectId($postId);
99 $contentId = PostMetaUtils::getContentId($postId);
100
101 if (! $projectId || ! $contentId) {
102 return false;
103 }
104
105 $url = sprintf('%s/projects/%d/content/%s', Environment::getApiUrl(), $projectId, $contentId);
106
107 $request = new Request('DELETE', $url);
108
109 return $this->callApi($postId, $request);
110 }
111
112 /**
113 * DELETE /projects/:id/content/:id.
114 *
115 * @since 4.1.0
116 *
117 * @param int[] $postIds Array of WordPress Post IDs.
118 *
119 * @throws \Exception
120 * @return int[] The Post IDs with deleted audio.
121 **/
122 public function batchDeleteAudio($postIds)
123 {
124 $contentIds = [];
125 $updatedPostIds = [];
126
127 foreach ($postIds as $postId) {
128 $projectId = PostMetaUtils::getProjectId($postId);
129
130 if (! $projectId) {
131 continue;
132 }
133
134 $contentId = PostMetaUtils::getContentId($postId);
135
136 if (! $contentId) {
137 continue;
138 }
139
140 $contentIds[$projectId][] = $contentId;
141 $updatedPostIds[] = $postId;
142 }
143
144 if (! count($contentIds)) {
145 throw new \Exception(__('None of the selected posts had valid BeyondWords audio data.', 'speechkit'));
146 }
147
148 if (count($contentIds) > 1) {
149 throw new \Exception(__('Batch delete can only be performed on audio belonging a single project.', 'speechkit')); // phpcs:ignore Generic.Files.LineLength.TooLong
150 }
151
152 $projectId = array_key_first($contentIds);
153
154 $url = sprintf('%s/projects/%d/content/batch_delete', Environment::getApiUrl(), $projectId);
155
156 $body = wp_json_encode(['ids' => $contentIds[$projectId]]);
157
158 $request = new Request('POST', $url, $body);
159
160 $args = array(
161 'blocking' => true,
162 'body' => $request->getBody(),
163 'headers' => $request->getHeaders(),
164 'method' => $request->getMethod(),
165 'sslverify' => true,
166 );
167
168 $response = wp_remote_request($request->getUrl(), $args);
169
170 // WordPress error performing API call
171 if (is_wp_error($response)) {
172 throw new \Exception($response->get_error_message());
173 }
174
175 $responseCode = wp_remote_retrieve_response_code($response);
176
177 if ($responseCode <= 299) {
178 // An OK response means all content IDs in the request were deleted
179 return $updatedPostIds;
180 } else {
181 // For non-OK responses we do not want to delete any custom fields,
182 // so return an empty array
183 return [];
184 }
185 }
186
187 /**
188 * GET /organization/languages
189 *
190 * @since 4.0.0 Introduced
191 * @since 4.0.2 Prefix endpoint with /organization
192 *
193 * @return array|object Array of voices or API error object.
194 **/
195 public function getLanguages()
196 {
197 $url = sprintf('%s/organization/languages', Environment::getApiUrl());
198
199 $request = new Request('GET', $url);
200
201 $args = array(
202 'blocking' => true,
203 'headers' => $request->getHeaders(),
204 'method' => $request->getMethod(),
205 'sslverify' => true,
206 );
207
208 $response = wp_remote_request($request->getUrl(), $args);
209
210 // WordPress error performing API call
211 if (is_wp_error($response) && get_the_ID()) {
212 $this->saveErrorMessage(
213 get_the_ID(),
214 $response->get_error_message(),
215 $response->get_error_code()
216 );
217
218 return false;
219 }
220
221 $responseBody = wp_remote_retrieve_body($response);
222
223 return json_decode($responseBody, true);
224 }
225
226 /**
227 * GET /organization/voices
228 *
229 * @since 4.0.0 Introduced
230 * @since 4.0.2 Prefix endpoint with /organization
231 *
232 * @param $languageId BeyondWords Language ID
233 *
234 * @return array|object Array of voices or API error object.
235 **/
236 public function getVoices($languageId)
237 {
238 $url = sprintf('%s/organization/voices?filter[language.id]=%s', Environment::getApiUrl(), urlencode(strval($languageId))); // phpcs:ignore Generic.Files.LineLength.TooLong
239
240 $request = new Request('GET', $url);
241
242 $args = array(
243 'blocking' => true,
244 'headers' => $request->getHeaders(),
245 'method' => $request->getMethod(),
246 'sslverify' => true,
247 );
248
249 $response = wp_remote_request($request->getUrl(), $args);
250
251 // WordPress error performing API call
252 if (is_wp_error($response) && get_the_ID()) {
253 $this->saveErrorMessage(
254 get_the_ID(),
255 $response->get_error_message(),
256 $response->get_error_code()
257 );
258
259 return false;
260 }
261
262 $responseBody = wp_remote_retrieve_body($response);
263
264 return json_decode($responseBody, true);
265 }
266
267 /**
268 * GET /projects/:id.
269 *
270 * @since 4.0.0
271 *
272 * @return Response|false Response, or false
273 **/
274 public function getProject()
275 {
276 $projectId = get_option('beyondwords_project_id');
277
278 if (! $projectId) {
279 return false;
280 }
281
282 $url = sprintf('%s/projects/%d', Environment::getApiUrl(), $projectId);
283
284 $request = new Request('GET', $url);
285
286 $args = array(
287 'blocking' => true,
288 'headers' => $request->getHeaders(),
289 'method' => $request->getMethod(),
290 'sslverify' => true,
291 );
292
293 $response = wp_remote_request($request->getUrl(), $args);
294
295 // WordPress error performing API call
296 if (is_wp_error($response) && get_the_ID()) {
297 $this->saveErrorMessage(
298 get_the_ID(),
299 $response->get_error_message(),
300 $response->get_error_code()
301 );
302
303 return false;
304 }
305
306 $responseBody = wp_remote_retrieve_body($response);
307
308 return json_decode($responseBody, true);
309 }
310
311 /**
312 * GET /projects/:id/player_settings.
313 *
314 * @since 4.0.0
315 *
316 * @param array $settings Associative array of player settings.
317 *
318 * @return Response|false Response, or false
319 **/
320 public function getPlayerSettings()
321 {
322 $projectId = get_option('beyondwords_project_id');
323
324 if (! $projectId) {
325 return false;
326 }
327
328 $url = sprintf('%s/projects/%d/player_settings', Environment::getApiUrl(), $projectId);
329
330 $request = new Request('GET', $url);
331
332 $args = array(
333 'blocking' => true,
334 'headers' => $request->getHeaders(),
335 'method' => $request->getMethod(),
336 'sslverify' => true,
337 );
338
339 $response = wp_remote_request($request->getUrl(), $args);
340
341 // WordPress error performing API call
342 if (is_wp_error($response) && get_the_ID()) {
343 $this->saveErrorMessage(
344 get_the_ID(),
345 $response->get_error_message(),
346 $response->get_error_code()
347 );
348
349 return false;
350 }
351
352 $responseBody = wp_remote_retrieve_body($response);
353
354 return json_decode($responseBody, true);
355 }
356
357 /**
358 * PUT /projects/:id/player_settings.
359 *
360 * @since 4.0.0
361 *
362 * @param array $settings Associative array of player settings.
363 *
364 * @return Response|false Response, or false
365 **/
366 public function updatePlayerSettings($settings)
367 {
368 $projectId = get_option('beyondwords_project_id');
369
370 if (! $projectId) {
371 return false;
372 }
373
374 $url = sprintf('%s/projects/%d/player_settings', Environment::getApiUrl(), $projectId);
375
376 $request = new Request('PUT', $url, $settings);
377
378 $args = array(
379 'blocking' => true,
380 'body' => wp_json_encode($settings),
381 'headers' => $request->getHeaders(),
382 'method' => $request->getMethod(),
383 'sslverify' => true,
384 );
385
386 $response = wp_remote_request($request->getUrl(), $args);
387
388 // WordPress error performing API call
389 if (is_wp_error($response) && get_the_ID()) {
390 $this->saveErrorMessage(
391 get_the_ID(),
392 $response->get_error_message(),
393 $response->get_error_code()
394 );
395
396 return false;
397 }
398
399 $responseBody = wp_remote_retrieve_body($response);
400
401 return json_decode($responseBody, true);
402 }
403
404 /**
405 * GET /projects/:id/video_settings.
406 *
407 * @since 4.1.0
408 *
409 * @param int $projectId BeyondWords Project ID.
410 *
411 * @return Response|false Response, or false
412 **/
413 public function getVideoSettings($projectId = null)
414 {
415 if (! $projectId) {
416 $projectId = get_option('beyondwords_project_id');
417
418 if (! $projectId) {
419 return false;
420 }
421 }
422
423 $url = sprintf('%s/projects/%d/video_settings', Environment::getApiUrl(), $projectId);
424
425 $request = new Request('GET', $url);
426
427 $args = array(
428 'blocking' => true,
429 'headers' => $request->getHeaders(),
430 'method' => $request->getMethod(),
431 'sslverify' => true,
432 );
433
434 $response = wp_remote_request($request->getUrl(), $args);
435
436 // WordPress error performing API call
437 if (is_wp_error($response) && get_the_ID()) {
438 $this->saveErrorMessage(
439 get_the_ID(),
440 $response->get_error_message(),
441 $response->get_error_code()
442 );
443
444 return false;
445 }
446
447 $responseBody = wp_remote_retrieve_body($response);
448
449 return json_decode($responseBody, true);
450 }
451
452 /**
453 * Call the BeyondWords API backend.
454 *
455 * @since 3.0.0
456 * @since 3.9.0 Stop saving the speechkit_status post meta - downgrades to plugin v2.x are no longer expected.
457 * @since 4.0.0 Removed hash comparison.
458 * @since 4.4.0 Handle 204 responses with no body.
459 *
460 * @param int $postId Post ID.
461 * @param Request $request Request.
462 *
463 * @return array|null|false JSON-decoded response body, or null for 204, or false on failure
464 **/
465 public function callApi($postId, $request)
466 {
467 $args = array(
468 'blocking' => true,
469 'body' => $request->getBody(),
470 'headers' => $request->getHeaders(),
471 'method' => $request->getMethod(),
472 'sslverify' => true,
473 );
474
475 // Reset any existing errors before making this API call
476 delete_post_meta($postId, 'speechkit_error_message');
477 delete_post_meta($postId, 'beyondwords_error_message');
478
479 $response = wp_remote_request($request->getUrl(), $args);
480
481 $errorMessage = '';
482
483 // WordPress error performing API call
484 if (is_wp_error($response)) {
485 $errorMessage = $response->get_error_message();
486
487 $this->saveErrorMessage($postId, $errorMessage, $response->get_error_code());
488
489 return false;
490 }
491
492 $responseCode = wp_remote_retrieve_response_code($response);
493
494 // 204 responses have no body
495 if ($responseCode === 204) {
496 return null;
497 }
498
499 $responseBodyJson = wp_remote_retrieve_body($response);
500 $responseBody = json_decode($responseBodyJson, true);
501
502 // Response had a HTTP error code (3XX, 4XX, 5XX)
503 if ($responseCode > 299) {
504 $errorMessage = $this->errorMessageFromResponse($response);
505
506 if (! $errorMessage) {
507 $errorMessage = sprintf(
508 /* translators: %s is replaced with the support email link */
509 esc_html__('API request error. Please contact %s.', 'speechkit'),
510 '<a href="mailto:[email protected]">[email protected]</a>'
511 );
512 }
513
514 $this->saveErrorMessage($postId, $errorMessage, $responseCode);
515
516 return false;
517 }
518
519 // Response was invalid JSON
520 if (json_last_error() !== JSON_ERROR_NONE) {
521 $errorMessage = sprintf(
522 /* translators: %s is replaced with the reason that JSON parsing failed */
523 __('Unable to parse JSON in BeyondWords API response. Reason: %s.', 'speechkit'),
524 // Don't allow any tags
525 wp_kses(json_last_error_msg(), [])
526 );
527
528 $this->saveErrorMessage($postId, $errorMessage, 500);
529
530 return false;
531 }
532
533 return $responseBody;
534 }
535
536 /**
537 * Handle API Error.
538 *
539 * @since 3.0.0
540 * @since 4.0.0 Removed hash comparison and display 403 errors.
541 * @since 4.4.0 Rename from error() to saveErrorMessage().
542 *
543 * @param int $postId Post ID.
544 * @param string $message Error Message.
545 * @param int $code Error Code.
546 *
547 * @throws \Exception
548 */
549 public function saveErrorMessage($postId, $message, $code = 0)
550 {
551 $error = sprintf(self::ERROR_FORMAT, $code, $message);
552
553 // Log the error message for this Post in the db
554 update_post_meta($postId, 'beyondwords_error_message', $error);
555 }
556
557 /**
558 * Error message from BeyondWords REST API response.
559 *
560 * @since 4.1.0
561 *
562 * @param mixed[] $response BeyondWords REST API response.
563 *
564 * @return string Error message.
565 */
566 public function errorMessageFromResponse($response)
567 {
568 $body = wp_remote_retrieve_body($response);
569 $body = json_decode($body, true);
570
571 if (is_array($body) && array_key_exists('errors', $body)) {
572 $messages = [];
573
574 foreach ($body['errors'] as $error) {
575 $messages[] = implode(" ", array_values($error));
576 }
577
578 $message = implode(", ", $messages);
579 } elseif (is_array($body) && array_key_exists('message', $body)) {
580 $message = $body['message'];
581 } else {
582 $message = wp_remote_retrieve_response_message($response);
583 }
584
585 return $message;
586 }
587
588 /**
589 * Get error message.
590 *
591 * @since 3.0.0
592 *
593 * @param error
594 *
595 * @return string
596 */
597 public function getErrorMessage($error)
598 {
599 return $error['message'];
600 }
601
602 /**
603 * Print admin notices.
604 *
605 * @since 3.0.0
606 *
607 * @return void
608 */
609 public function adminNotices()
610 {
611 $screen = get_current_screen();
612
613 // Only add for enabled Posts screen
614 $postTypes = SettingsUtils::getSupportedPostTypes();
615
616 if (! in_array($screen->id, $postTypes)) {
617 return;
618 }
619
620 $errorMessage = PostMetaUtils::getErrorMessage(get_the_ID());
621
622 if (!$errorMessage) {
623 return;
624 }
625
626 ?>
627 <div class="notice notice-error">
628 <p>
629 <span class="dashicons dashicons-controls-volumeon"></span>
630 <?php echo esc_html($errorMessage); ?>
631 </p>
632 </div>
633 <?php
634 }
635 }
636