PluginProbe
BeyondWords – AI audio for publishers / 4.2.0
BeyondWords – AI audio for publishers v4.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 4.7.0 All 43 releases
speechkit / src / Core / ApiClient.php

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

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