PluginProbe
WebTotem Security / trunk
WebTotem Security vtrunk
3.0.2 3.0.1 3.0.0 trunk 1.0 1.1 1.2 1.3 1.3.1 1.3.2 1.3.3 2.0 2.1 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.1 2.2.2 2.2.3 All 110 releases
wt-security / lib / API.php

API.php in WebTotem Security trunk, at lib/API.php

1,016 lines 28.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('WEBTOTEM_INIT') || WEBTOTEM_INIT !== true) {
4 if (!headers_sent()) {
5 header('HTTP/1.1 403 Forbidden');
6 }
7 die("Protected By WebTotem!");
8 }
9
10 /**
11 * WebTotem API class.
12 *
13 * Mostly contains wrappers for API methods. Check and send methods.
14 *
15 * @version 1.0
16 * @copyright (C) 2022 WebTotem team (http://wtotem.com)
17 * @license GNU/GPL: http://www.gnu.org/copyleft/gpl.html
18 */
19 class WebTotemAPI extends WebTotem
20 {
21
22 /**
23 * HTTP status of the most recent API call, 0 when the request never landed.
24 *
25 * @var int
26 */
27 protected static $last_status = 0;
28
29 /**
30 * HTTP status of the most recent API call.
31 *
32 * @return int
33 * Status code, or 0 when the request did not reach the server.
34 */
35 public static function getLastStatus()
36 {
37 return (int) self::$last_status;
38 }
39
40 /**
41 * Raises a notification unless the caller asked to stay quiet.
42 *
43 * Probing calls (such as the WebSocket ticket, which is expected to be
44 * missing on older API builds) must not spam the admin with errors.
45 *
46 * @param bool $silent
47 * TRUE to swallow the notification.
48 * @param string $type
49 * Notification type.
50 * @param string $message
51 * Notification text.
52 *
53 * @return void
54 */
55 protected static function notify($silent, $type, $message)
56 {
57 if (!$silent) {
58 WebTotemOption::setNotification($type, $message);
59 }
60 }
61
62
63 /**
64 * Method for getting an auth token.
65 *
66 * @param string $api_key
67 * Application programming interface key.
68 *
69 * @return bool|string
70 * Returns auth status
71 */
72 public static function auth($api_key, $repeat = FALSE)
73 {
74 $domain = WEBTOTEM_SITE_DOMAIN;
75
76 if (empty($api_key)) {
77 return FALSE;
78 }
79
80 $data = ['api_key' => $api_key, 'site' => $domain];
81 $result = self::sendRequest('auth/sign-in/api-key', $data, 'POST', FALSE, TRUE);
82
83 if($result === null){
84 WebTotemOption::setNotification('warning' , __('Authorization failed. The server may be temporarily unavailable', 'wtotem'));
85 }
86
87 if (isset($result['access_token'])) {
88 $auth_token = $result['access_token'];
89 if(!WebTotemOption::isActivated()){
90 WebTotemOption::login(['token' => $auth_token, 'api_key' => $api_key]);
91 WebTotemAgentManager::postdelete();
92 } else {
93 WebTotemOption::refreshToken($auth_token);
94 }
95
96 return 'success';
97 } elseif (isset($result['message']) and $result['message'] == 'invalid credentials') {
98 WebTotemOption::logout();
99 }
100
101 if($repeat == false){
102 //self::checkEndpoint();
103 return self::auth($api_key, true);
104 }
105
106 return FALSE;
107 }
108
109 /**
110 * Method for getting API url.
111 *
112 * @return string|bool
113 * API url
114 */
115 public static function getApiUrl()
116 {
117 return 'https://app.wtotem.com';
118 }
119
120 /**
121 * Method for getting the WebSocket endpoint url.
122 *
123 * @return string
124 * WebSocket url, without any credentials.
125 */
126 public static function getWsUrl()
127 {
128 $api_url = WebTotemOption::getOption('api_url');
129 if (!$api_url) {
130 $api_url = self::getApiUrl();
131 }
132
133 return preg_replace('#^http#i', 'ws', rtrim($api_url, '/')) . '/api/v1/ws';
134 }
135
136
137 /**
138 * Get site info from API server.
139 *
140 * @param string $attempt
141 * Is the request an attempt to get host data.
142 *
143 * @return array
144 * Returns host data.
145 */
146 public static function siteInfo($attempt = FALSE)
147 {
148 if (self::isMultiSite()) {
149 $host['id'] = WebTotemOption::getSessionOption('host_id');
150 $host['name'] = WebTotemOption::getSessionOption('host_name');
151
152 if ($host['id']) {
153 return $host;
154 }
155 }
156
157 $host = WebTotemOption::getHost();
158
159 if ($host['id']) {
160 return $host;
161 }
162
163 // if (self::isMultiSite()) {
164 // $sites = get_sites();
165 // foreach ($sites as $site) {
166 // $domain = untrailingslashit($site->domain . $site->path);
167 // self::addSite($domain);
168 // }
169 //
170 // if (!$attempt) {
171 // return self::siteInfo(TRUE);
172 // }
173 // } else {
174 // $domain = WEBTOTEM_SITE_DOMAIN;
175 // return self::addSite($domain);
176 // }
177 $domain = WEBTOTEM_SITE_DOMAIN;
178 return self::addSite($domain);
179
180 // return [];
181 }
182
183 /**
184 * Method for adding a site to the WebTotem platform.
185 *
186 * @param string $domain
187 * Domain to add.
188 *
189 * @return array
190 * Returns host data.
191 */
192 public static function addSite($domain)
193 {
194 if (function_exists('idn_to_utf8')) {
195 $domain = idn_to_utf8($domain);
196 }
197
198 // Checking if the site has been added to the WebTotem.
199 if(!$host = self::getHostID($domain)){
200 $host = self::getHostID('www.' . $domain);
201 }
202
203 if($host['id']){
204 // Remember the binding: otherwise every page load asks the API
205 // for the host id again (and again for the www. variant).
206 WebTotemOption::setHost($host['hostname'], $host['id']);
207
208 return [
209 'id' => $host['id'],
210 'name' => $host['hostname'],
211 ];
212 }
213
214 // If the site is not added then try to add.
215 $data = ['hosts' => [$domain]];
216 $response = self::sendRequest('hosts', $data, 'POST', TRUE);
217
218 if (isset($response['message'])) {
219 WebTotemOption::setNotification('error', __('Failed to add the site to the WebTotem platform.', 'wtotem'));
220 } else {
221 if ($response['data']['added']) {
222 // If it added, save site ID.
223 $host = self::getHostID($domain);
224 WebTotemOption::setHost($domain, $host['id']);
225 return [
226 'id' => $host['id'],
227 'name' => $host['hostname'],
228 ];
229 }
230 }
231 return [];
232 }
233
234 /**
235 * Get all sites from API.
236 *
237 * @param string $page_num
238 * Mark for loading data.
239 * @param string $limit
240 * Limit of sites to loading.
241 *
242 * @return array
243 * Returns host data.
244 */
245 public static function getSites($page_num = 1, $page_size = 15, $status = 'active', $hostname = '')
246 {
247 $query = [
248 'page_num' => $page_num,
249 'page_size' => $page_size,
250 'status' => $status,
251 ];
252
253 if ($hostname !== '') {
254 $query['hostname'] = $hostname;
255 }
256
257 $result = self::sendRequest('hosts', $query, 'GET', TRUE);
258
259 // The API answers { "data": { "hosts": [...], "host_limit": n, "can_defrost": bool } }.
260 $payload = self::payload($result);
261
262 return isset($payload['hosts']) && is_array($payload['hosts']) ? $payload['hosts'] : [];
263 }
264
265 /**
266 * Reads the payload out of the API response envelope.
267 *
268 * Most endpoints answer { "data": ... }; a few older builds used "Data".
269 *
270 * @param mixed $response
271 * Decoded API response.
272 *
273 * @return array
274 * Payload, or an empty array when the response carried none.
275 */
276 protected static function payload($response)
277 {
278 if (!is_array($response)) {
279 return [];
280 }
281
282 foreach (['data', 'Data'] as $key) {
283 if (isset($response[$key]) && is_array($response[$key])) {
284 return $response[$key];
285 }
286 }
287
288 return [];
289 }
290
291 /**
292 * Requests a single-use ticket for the browser WebSocket connection.
293 *
294 * The ticket replaces the access token that used to be printed into the
295 * page: it is short-lived, may be used once, and is bound to the browser
296 * Origin it was issued for.
297 *
298 * @param string $origin
299 * Browser origin the ticket is issued for, e.g. https://example.com.
300 *
301 * @return string
302 * The ticket, or an empty string when it could not be obtained.
303 */
304 public static function getWSTicket($origin = '')
305 {
306 $query = [];
307 if (is_string($origin) && $origin !== '') {
308 // add_query_arg() does not encode values; the origin carries "://".
309 $query['origin'] = rawurlencode($origin);
310 }
311
312 $response = self::sendRequest('ws/ticket', $query, 'GET', TRUE, FALSE, TRUE);
313
314 if (is_array($response) && !empty($response['ticket'])) {
315 return (string) $response['ticket'];
316 }
317
318 // Tolerate a wrapped answer in case the endpoint starts using the envelope.
319 $payload = self::payload($response);
320
321 return !empty($payload['ticket']) ? (string) $payload['ticket'] : '';
322 }
323
324 /**
325 * Check the site's presence in the list on the API side.
326 *
327 * @param string $site
328 * The domain we want to check.
329 *
330 * @return array
331 * Returns host data.
332 */
333 public static function getHostID($site)
334 {
335 $result = self::sendRequest('hosts/id', ['hostname' => $site], 'GET', TRUE);
336
337 if (isset($result['data'])) {
338 WebTotemOption::setOptions(['config_id' => $result['data']['config_id']]);
339 return ['id' => $result['data']['host_id'], 'hostname' => $site];
340 }
341
342 return ['id' => '', 'hostname' => ''];
343 }
344
345
346 /**
347 * Method to get the agents file names and AM file link.
348 *
349 * @param string $host_id
350 * Host id on WebTotem.
351 *
352 * @return array
353 * Returns agents files data.
354 */
355 public static function getAgentsFiles($host_id)
356 {
357
358 // if (WebTotem::isMultiSite()) {
359 // $all_hosts = WebTotemOption::getOption('all_hosts');
360 // $all_hosts = $all_hosts ? json_decode($all_hosts, true) : [];
361 //
362 // $siteIdsArray = $all_hosts ? array_values($all_hosts) : [];
363 // $siteIds = $siteIdsArray ? addslashes(WebTotem::convertArrayToString($siteIdsArray)) : '';
364 //
365 // $payload = '{"query":"mutation { auth { am { installMultisite(mainSiteId: \"' . $host_id . '\", siteIds: [' . $siteIds . ']){ downloadLink, amFilename, wafFilename, avFilename } } } }"}';
366 // $response = self::sendRequest($payload, TRUE);
367 //
368 // if (isset($response['data']['auth']['am']['installMultisite'])) {
369 // return $response['data']['auth']['am']['installMultisite'];
370 // }
371 // } else {
372 $response = self::sendRequest('/agents/' . $host_id . '/install', [], 'POST', TRUE);
373
374 if (isset($response['data'])) {
375 return $response['data'];
376 }
377 // }
378 return [];
379 }
380
381 /**
382 * Add secondary MultiSite host.
383 *
384 * @param $new_sites
385 * An array with sites to add.
386 *
387 * @return void.
388 */
389 public static function addMultiSiteNewSites($new_sites)
390 {
391
392 }
393
394 /**
395 * Get the date of creation of the site.
396 *
397 * @param string $site
398 * The domain we want to check.
399 *
400 * @return string|bool
401 * Returns host data.
402 */
403 public static function getGetSiteAddedDate($site)
404 {
405 if (!$site) {
406 return FALSE;
407 }
408
409 $hosts = self::getSites(1, 1, 'active', $site);
410
411 foreach ($hosts as $host) {
412 if (!empty($host['created_at'])) {
413 return $host['created_at'];
414 }
415 }
416
417 return FALSE;
418 }
419
420 /**
421 * Remove secondary MultiSite host.
422 *
423 * @param $host_id
424 * Host id on WebTotem.
425 *
426 * @return bool
427 * Returns result removing host.
428 */
429 public static function removeMultiSiteHost($host_id)
430 {
431
432 return false;
433 }
434
435 /**
436 * Method to get agents (AM, WAF, AV) statuses.
437 *
438 * @return array
439 * Returns agents statuses data.
440 */
441 public static function getAgentsStatusesFromAPI()
442 {
443 $config_id = WebTotemOption::getOption('config_id');
444 $response = self::sendRequest('/agents/' . $config_id . '/status', [], 'GET', TRUE);
445
446 if (isset($response['data'])) {
447 return $response['data'];
448 }
449
450 return [];
451 }
452
453 /**
454 * Method for get monitoring data.
455 *
456 * @param string $host_id
457 * Host id on WebTotem.
458 * @param int|array $days
459 * For what period data is needed.
460 *
461 * @return array
462 * Returns all data.
463 */
464 public static function getMonitoringData($host_id)
465 {
466 $response = self::sendRequest('/dashboard/monitoring/' . $host_id . '/results', [], 'GET', TRUE);
467
468 if (isset($response['data'])) {
469 return $response['data'];
470 }
471
472 return [];
473 }
474
475 /**
476 * Method to get firewall data.
477 *
478 * @param int $limit
479 * Limit on the number of records.
480 * @param string $page
481 * Page for loading data.
482 * @param int|array $days
483 * For what period data is needed.
484 *
485 * @return array
486 * Returns firewall data.
487 */
488 public static function getFirewall($limit = 20, $page = 1, $days = 365)
489 {
490 $period = WebTotem::getPeriod($days);
491
492 $config_id = WebTotemOption::getOption('config_id');
493 $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/logs', [
494 'page_num' => $page,
495 'page_size' => $limit,
496 'from' => $period['from'],
497 'to' => $period['to']
498 ], 'GET', TRUE);
499
500
501 if (isset($response['data'])) {
502 return $response['data'];
503 }
504
505 return [];
506 }
507
508 /**
509 * Method to get firewall chart data.
510 *
511 * @param int $days
512 * For what period data is needed.
513 *
514 * @return array
515 * Returns firewall chart data.
516 */
517 public static function getFirewallStatistics($days = 7)
518 {
519 $period = WebTotem::getPeriod($days);
520
521 $config_id = WebTotemOption::getOption('config_id');
522 $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/statistics', [
523 'from' => $period['from'],
524 'to' => $period['to']
525 ], 'GET', TRUE);
526
527
528 if (isset($response['data'])) {
529 return $response['data'];
530 }
531
532 return [];
533 }
534
535 /**
536 * Method to get firewall settings.
537 *
538 * @return array
539 * Returns information whether the request was successful.
540 */
541 public static function getFirewallSettings()
542 {
543 $config_id = WebTotemOption::getOption('config_id');
544 $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/configs', [], 'GET', TRUE);
545 if (isset($response['data'])) {
546 return $response['data'];
547 }
548
549 return [];
550 }
551
552 /**
553 * Method to set firewall settings.
554 *
555 * @param array $settings
556 * User-specified settings.
557 *
558 * @return array
559 * Returns information whether the request was successful.
560 */
561 public static function setFirewallSettings(array $settings)
562 {
563 $config_id = WebTotemOption::getOption('config_id');
564 return self::sendRequest('/dashboard/firewall/' . $config_id . '/configs', $settings, 'PATCH', TRUE);
565 }
566
567
568 /**
569 * Method to get antivirus history data.
570 *
571 * @param int $page_num
572 * Page number.
573 * @param int $page_size
574 * Number of entries per page.
575 *
576 * @return array
577 * Returns antivirus history data.
578 */
579 public static function getAntivirusHistory($page_num = 1, $page_size = 10)
580 {
581 $config_id = WebTotemOption::getOption('config_id');
582 $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/history', [
583 'page_num' => $page_num,
584 'page_size' => $page_size
585 ], 'GET', TRUE);
586
587 if (isset($response['data'])) {
588 return $response['data'];
589 }
590 return [];
591 }
592
593 /**
594 * Method to get antivirus history details data.
595 *
596 * @param int $scan_id
597 * Scan ID.
598 * @param int $page_num
599 * Page number.
600 * @param int $page_size
601 * Number of entries per page.
602 *
603 * @return array
604 * Returns antivirus history data.
605 */
606 public static function getAntivirusHistoryDetails($scan_id, $page_num = 1, $page_size = 10)
607 {
608 $config_id = WebTotemOption::getOption('config_id');
609 $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/history/' . $scan_id . '/details', [
610 'page_num' => $page_num,
611 'page_size' => $page_size
612 ], 'GET', TRUE);
613
614
615 if (isset($response['data'])) {
616 return $response['data'];
617 }
618 return [];
619 }
620
621 /**
622 * Method to get quarantine data.
623 *
624 * @param int $page_num
625 * Page number.
626 * @param int $page_size
627 * Number of entries per page.
628 *
629 * @return array
630 * Returns quarantine data.
631 */
632 public static function getAntivirusCurrentDetails($page_num = 1, $page_size = 5)
633 {
634 $config_id = WebTotemOption::getOption('config_id');
635 $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/current/details', [
636 'page_num' => $page_num,
637 'page_size' => $page_size
638 ], 'GET', TRUE);
639
640 if (isset($response['data'])) {
641 return $response['data'];
642 }
643 return [];
644 }
645
646
647 /**
648 * Method to force check Antivirus.
649 *
650 * @return mixed
651 * Returns information whether the request was successful.
652 */
653 public static function forceCheckAV()
654 {
655 $config_id = WebTotemOption::getOption('config_id');
656 return self::sendRequest('/dashboard/antivirus/' . $config_id . '/check', [], 'POST', TRUE);
657 }
658
659
660 /**
661 * Method to force check services.
662 *
663 * @param string $host_id
664 * Host id on WebTotem.
665 * @param string $module_name
666 * Service that needs to be checked.
667 *
668 * @return array
669 * Returns information whether the request was successful.
670 */
671 public static function forceCheck($host_id, $module_name)
672 {
673 return self::sendRequest('/dashboard/hosts/' . $host_id . '/check', ['module_name' => $module_name], 'POST', TRUE);
674 }
675
676 /**
677 * Method to get quarantine data.
678 *
679 * @param int $page_num
680 * Page number.
681 * @param int $page_size
682 * Number of entries per page.
683 *
684 * @return array
685 * Returns quarantine data.
686 */
687 public static function getQuarantineList($page_num = 1, $page_size = 5)
688 {
689 $config_id = WebTotemOption::getOption('config_id');
690 $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine', [
691 'page_num' => $page_num,
692 'page_size' => $page_size
693 ], 'GET', TRUE);
694
695 if (isset($response['data'])) {
696 return $response['data'];
697 }
698 return [];
699 }
700
701 /**
702 * Method to move file to quarantine.
703 *
704 * @param string $file_id
705 * File ID.
706 *
707 * @return array
708 * Returns information whether the request was successful.
709 */
710 public static function moveToQuarantine($file_id)
711 {
712 $config_id = WebTotemOption::getOption('config_id');
713 return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/to-quarantine', [
714 'file_id' => $file_id,
715 ], 'POST', TRUE);
716 }
717
718 /**
719 * Method to move file from quarantine.
720 *
721 * @param string $file_id
722 * File ID.
723 *
724 * @return array
725 * Returns information whether the request was successful.
726 */
727 public static function moveFromQuarantine($file_id)
728 {
729 $config_id = WebTotemOption::getOption('config_id');
730 return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/from-quarantine', [
731 'file_id' => $file_id,
732 ], 'POST', TRUE);
733 }
734
735 /**
736 * Method to get allow/deny ip list.
737 *
738 * @param string $type
739 * Type of ip list
740 *
741 * @return array|bool
742 * Returns ip allow/deny lists.
743 */
744 public static function getIpLists($type = 'blacklist')
745 {
746 $config_id = WebTotemOption::getOption('config_id');
747 $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/configs/iplist', [
748 'type' => $type,
749 ], 'GET', TRUE);
750
751 if (isset($response['data'])) {
752 return $response['data'];
753 }
754
755 return [];
756 }
757
758 /**
759 * Method to add ip to allow/deny list.
760 *
761 * @param string $ip
762 * Ip address.
763 * @param string $type
764 * Allow or deny type.
765 *
766 * @return bool
767 * Returns information whether the request was successful.
768 */
769 public static function addIpToList($ips, $type)
770 {
771 $config_id = WebTotemOption::getOption('config_id');
772 self::sendRequest('/dashboard/firewall/' . $config_id . '/configs/iplist?type=' . $type, [
773 'ip' => array_filter($ips),
774 ], 'POST', TRUE);
775
776 return true;
777 }
778
779 /**
780 * Method to remove ip from allow/deny list by id.
781 *
782 * @param string $ip
783 * Ip address.
784 * @param string $type
785 * Allow or deny type.
786 *
787 * @return bool
788 * Returns information whether the request was successful.
789 */
790 public static function removeIpFromList($ip, $type)
791 {
792 $config_id = WebTotemOption::getOption('config_id');
793 self::sendRequest('/dashboard/firewall/' . $config_id . '/configs/iplist?type=' . $type, [
794 'ip' => $ip,
795 ], 'DELETE', TRUE);
796
797 return true;
798 }
799
800 /**
801 * Sends a REST API request to the WebTotem API server.
802 *
803 * @param string $endpoint
804 * REST API endpoint (e.g., 'scan', 'status', etc.).
805 * @param array $data
806 * Associative array of data to send as JSON body or query parameters.
807 * @param string $method
808 * HTTP method: GET, POST, PUT, DELETE (default is POST).
809 * @param bool $useToken
810 * Whether to include the auth token.
811 * @param bool $retry
812 * Used to prevent recursion on token renewal.
813 *
814 * @return array|null
815 * API response as an associative array, or null on failure.
816 */
817 protected static function sendRequest($endpoint, $data = [], $method = 'POST', $useToken = false, $retry = false, $silent = false)
818 {
819 self::$last_status = 0;
820 $api_key = WebTotemOption::getOption('api_key');
821
822 // Get or initialize the API URL.
823 $api_url = WebTotemOption::getOption('api_url');
824 if (!$api_url) {
825 $api_url = self::getApiUrl();
826 WebTotemOption::setOptions(['api_url' => $api_url]);
827 }
828
829 // The API previously answered 429: respect the requested pause instead of
830 // hammering the server (and getting the whole site throttled).
831 $throttled_until = (int) WebTotemOption::getOption('api_retry_after');
832 if ($throttled_until > time()) {
833 self::notify($silent, 'warning', sprintf(
834 /* translators: %d: number of seconds to wait. */
835 __('Too many requests to the WebTotem API. Please try again in %d seconds.', 'wtotem'),
836 $throttled_until - time()
837 ));
838
839 return NULL;
840 }
841
842 $auth_token = NULL;
843 if ($useToken) {
844 $auth_token = WebTotemOption::getOption('auth_token');
845 $auth_token_expired = (int) WebTotemOption::getOption('auth_token_expired');
846
847 if ((!$auth_token || $auth_token_expired <= time()) && !$retry) {
848 $result = self::auth($api_key);
849 if ($result === 'success') {
850 return self::sendRequest($endpoint, $data, $method, $useToken, TRUE, $silent);
851 }
852 }
853 }
854
855 $url = rtrim($api_url, '/') . '/api/v1/' . ltrim($endpoint, '/');
856
857 $args = [
858 'timeout' => 60,
859 'sslverify' => TRUE,
860 'headers' => [
861 'Accept' => 'application/json',
862 'Content-Type' => 'application/json',
863 'source' => 'WORDPRESS',
864 ],
865 ];
866
867 if ($auth_token) {
868 $args['headers']['Authorization'] = "Bearer $auth_token";
869 }
870
871 if (strtoupper($method) === 'GET') {
872 $url = add_query_arg($data, $url);
873 } else {
874 $args['body'] = wp_json_encode($data);
875 }
876
877 $response = wp_remote_request($url, array_merge($args, ['method' => strtoupper($method)]));
878
879 if (is_wp_error($response)) {
880 self::notify($silent, 'error', WebTotem::messageForHuman(
881 'SERVER UNAVAILABLE: ' . $response->get_error_message()
882 ));
883
884 return NULL;
885 }
886
887 $code = (int) wp_remote_retrieve_response_code($response);
888 self::$last_status = $code;
889 $body = wp_remote_retrieve_body($response);
890 $decoded = json_decode($body, TRUE);
891
892 if (!is_array($decoded)) {
893 $decoded = [];
894 }
895
896 $error_message = isset($decoded['message']) ? (string) $decoded['message'] : '';
897
898 // Terminal account states: show the dedicated page and stop.
899 if ($error_message !== '') {
900 if (stripos($error_message, 'Password expired') !== FALSE) {
901 wtotem_error_page(['errors' => 'PASSWORD_EXPIRED']);
902 exit();
903 }
904
905 if (stripos($error_message, 'API_KEY_DEACTIVATED') !== FALSE) {
906 wtotem_error_page(['errors' => 'TARIFF_EXPIRED']);
907 exit();
908 }
909 }
910
911 // 429 Too Many Requests: honour Retry-After and never re-authorize.
912 if ($code === 429) {
913 $delay = self::parseRetryAfter(wp_remote_retrieve_header($response, 'retry-after'));
914 WebTotemOption::setOptions(['api_retry_after' => time() + $delay]);
915 self::notify($silent, 'warning', sprintf(
916 /* translators: %d: number of seconds to wait. */
917 __('Too many requests to the WebTotem API. Please try again in %d seconds.', 'wtotem'),
918 $delay
919 ));
920
921 return NULL;
922 }
923
924 // 401 Unauthorized: the token is gone or expired. Re-login by API key once.
925 if ($code === 401) {
926 if (!$retry && $api_key && self::auth($api_key, TRUE) === 'success') {
927 return self::sendRequest($endpoint, $data, $method, $useToken, TRUE, $silent);
928 }
929
930 self::notify($silent, 'error', WebTotem::messageForHuman(
931 $error_message !== '' ? $error_message : 'invalid credentials'
932 ));
933
934 return $decoded;
935 }
936
937 // 403 Forbidden: authenticated, but not allowed. Re-login would not help.
938 if ($code === 403) {
939 if (stripos($error_message, 'USERHOST_NOT_BELONG_TO_USER') !== FALSE) {
940 self::forgetHost();
941 } else {
942 self::notify($silent, 'error', WebTotem::messageForHuman(
943 $error_message !== '' ? $error_message : 'access denied'
944 ));
945 }
946
947 return $decoded;
948 }
949
950 // Older API builds answer 200 with an error message in the body.
951 if ($error_message !== '') {
952 if ($error_message === 'invalid credentials') {
953 if (!$retry && $api_key && self::auth($api_key, TRUE) === 'success') {
954 return self::sendRequest($endpoint, $data, $method, $useToken, TRUE, $silent);
955 }
956 } elseif (stripos($error_message, 'USERHOST_NOT_BELONG_TO_USER') !== FALSE) {
957 self::forgetHost();
958 } else {
959 self::notify($silent, 'error', WebTotem::messageForHuman($error_message));
960 }
961 }
962
963 return $decoded;
964 }
965
966 /**
967 * Reads the Retry-After header into a number of seconds.
968 *
969 * The header is either a number of seconds or an HTTP date.
970 *
971 * @param string $header
972 * Raw Retry-After header value.
973 *
974 * @return int
975 * Seconds to wait, clamped to a sane range.
976 */
977 protected static function parseRetryAfter($header)
978 {
979 $delay = 0;
980 $header = is_string($header) ? trim($header) : '';
981
982 if ($header !== '') {
983 if (ctype_digit($header)) {
984 $delay = (int) $header;
985 } else {
986 $timestamp = strtotime($header);
987 if ($timestamp !== FALSE) {
988 $delay = $timestamp - time();
989 }
990 }
991 }
992
993 if ($delay < 1) {
994 $delay = 60;
995 }
996
997 // Never park the plugin for longer than an hour.
998 return min($delay, HOUR_IN_SECONDS);
999 }
1000
1001 /**
1002 * Drops the locally stored host binding after the API disowned it.
1003 *
1004 * @return void
1005 */
1006 protected static function forgetHost()
1007 {
1008 if (WebTotem::isMultiSite()) {
1009 WebTotemOption::clearAllHosts();
1010 }
1011
1012 WebTotemOption::clearOptions(['host_id', 'host_name']);
1013 }
1014
1015 }
1016