PluginProbe
WebTotem Security / 3.0.2
WebTotem Security v3.0.2
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
← All changes | lib/API.php +292 -564 3.0.13.0.2 View file →
@@ -19,8 +19,49 @@
19 19 class WebTotemAPI extends WebTotem
20 20 {
21 21
22 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 + /**
23 64 * Method for getting an auth token.
24 65 *
25 66 * @param string $api_key
26 67 * Application programming interface key.
@@ -75,9 +116,25 @@
75 116 {
76 117 return 'https://app.wtotem.com';
77 118 }
78 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 + }
79 132
133 + return preg_replace('#^http#i', 'ws', rtrim($api_url, '/')) . '/api/v1/ws';
134 + }
135 +
136 +
80 137 /**
81 138 * Get site info from API server.
82 139 *
83 140 * @param string $attempt
@@ -143,14 +200,18 @@
143 200 $host = self::getHostID('www.' . $domain);
144 201 }
145 202
146 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 +
147 208 return [
148 209 'id' => $host['id'],
149 210 'name' => $host['hostname'],
150 211 ];
151 212 }
152 -
213 +
153 214 // If the site is not added then try to add.
154 215 $data = ['hosts' => [$domain]];
155 216 $response = self::sendRequest('hosts', $data, 'POST', TRUE);
156 217
@@ -180,20 +241,88 @@
180 241 *
181 242 * @return array
182 243 * Returns host data.
183 244 */
184 - public static function getSites($page_num = 1, $page_size = 15, $status = 'active')
245 + public static function getSites($page_num = 1, $page_size = 15, $status = 'active', $hostname = '')
185 246 {
186 - $result = self::sendRequest('hosts', ['page_num' => $page_num, 'page_size' => $page_size, 'status' => $status], 'GET', TRUE);
247 + $query = [
248 + 'page_num' => $page_num,
249 + 'page_size' => $page_size,
250 + 'status' => $status,
251 + ];
187 252
188 - if (isset($result['Data'])) {
189 - return $result['Data'];
253 + if ($hostname !== '') {
254 + $query['hostname'] = $hostname;
190 255 }
191 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 +
192 288 return [];
193 289 }
194 290
195 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 + /**
196 325 * Check the site's presence in the list on the API side.
197 326 *
198 327 * @param string $site
199 328 * The domain we want to check.
@@ -272,16 +401,21 @@
272 401 * Returns host data.
273 402 */
274 403 public static function getGetSiteAddedDate($site)
275 404 {
276 - $payload = '{"query": "query getSites { auth { viewer { sites { list(filter: { search: \"'. $site .'\" }) { edges{ node{ createdAt } } } } } } }" }';
277 - $result = self::sendRequest($payload, true);
405 + if (!$site) {
406 + return FALSE;
407 + }
278 408
279 - if (isset($result['data']['auth']['viewer']['sites']['list']['edges'][0]['node']['createdAt'])) {
280 - return $result['data']['auth']['viewer']['sites']['list']['edges'][0]['node']['createdAt'];
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 + }
281 415 }
282 416
283 - return false;
417 + return FALSE;
284 418 }
285 419
286 420 /**
287 421 * Remove secondary MultiSite host.
@@ -316,25 +450,8 @@
316 450 return [];
317 451 }
318 452
319 453 /**
320 - * Method to get user time zone.
321 - *
322 - * @return string|bool
323 - * Returns time zone data.
324 - */
325 - public static function getTimeZone()
326 - {
327 - $payload = '{"query":"query { auth { viewer{ timezone } } } "}';
328 - $response = self::sendRequest($payload, TRUE);
329 -
330 - if (isset($response['data']['auth']['viewer']['timezone'])) {
331 - return $response['data']['auth']['viewer']['timezone'];
332 - }
333 - return FALSE;
334 - }
335 -
336 - /**
337 454 * Method for get monitoring data.
338 455 *
339 456 * @param string $host_id
340 457 * Host id on WebTotem.
@@ -343,9 +460,9 @@
343 460 *
344 461 * @return array
345 462 * Returns all data.
346 463 */
347 - public static function getMonitoringData($host_id, $days = 7)
464 + public static function getMonitoringData($host_id)
348 465 {
349 466 $response = self::sendRequest('/dashboard/monitoring/' . $host_id . '/results', [], 'GET', TRUE);
350 467
351 468 if (isset($response['data'])) {
@@ -354,32 +471,9 @@
354 471
355 472 return [];
356 473 }
357 474
358 -
359 475 /**
360 - * Method for get all the site security data.
361 - *
362 - * @param string $host_id
363 - * Host id on WebTotem.
364 - *
365 - * @return array
366 - * Returns all data.
367 - */
368 - public static function getMonitoring($host_id)
369 - {
370 -
371 - $payload = '{"query":"query($id: ID!) { auth { viewer { sites { one(id: $id) { domain { lastScanResult { isTaken hasSite redirectLink isLocal protection ips { ip location } status time } } sslResults{ results{ certStatus certIssuerName certExpiryDate certIssueDate } } ssl { status daysLeft expiryDate issueDate } reputation { status lastTest { time } virusList { virus{ type path } antiVirus } } } } } } }","variables":{"id":"' . $host_id . '"}}';
372 - $response = self::sendRequest($payload, TRUE);
373 -
374 - if (isset($response['data']['auth']['viewer']['sites']['one'])) {
375 - return $response['data']['auth']['viewer']['sites']['one'];
376 - }
377 -
378 - return [];
379 - }
380 -
381 - /**
382 476 * Method to get firewall data.
383 477 *
384 478 * @param int $limit
385 479 * Limit on the number of records.
@@ -606,282 +700,40 @@
606 700
607 701 /**
608 702 * Method to move file to quarantine.
609 703 *
610 - * @param string $path
611 - * Path to the file.
704 + * @param string $file_id
705 + * File ID.
612 706 *
613 707 * @return array
614 708 * Returns information whether the request was successful.
615 709 */
616 - public static function moveToQuarantine($path)
710 + public static function moveToQuarantine($file_id)
617 711 {
618 712 $config_id = WebTotemOption::getOption('config_id');
619 -
620 - return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/' . $path . '/to-quarantine',
621 - [], 'POST', TRUE);
713 + return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/to-quarantine', [
714 + 'file_id' => $file_id,
715 + ], 'POST', TRUE);
622 716 }
623 717
624 718 /**
625 719 * Method to move file from quarantine.
626 720 *
627 - * @param string $path
628 - * Path to the file.
721 + * @param string $file_id
722 + * File ID.
629 723 *
630 724 * @return array
631 725 * Returns information whether the request was successful.
632 726 */
633 - public static function moveFromQuarantine($path)
727 + public static function moveFromQuarantine($file_id)
634 728 {
635 729 $config_id = WebTotemOption::getOption('config_id');
636 - return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/' . $path . '/from-quarantine',
637 - [], 'POST', TRUE);
730 + return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/from-quarantine', [
731 + 'file_id' => $file_id,
732 + ], 'POST', TRUE);
638 733 }
639 734
640 735 /**
641 - * Method to get server status data.
642 - *
643 - * @param string $host_id
644 - * Host id on WebTotem.
645 - * @param int|array $days
646 - * For what period data is needed.
647 - *
648 - * @return array
649 - * Returns server status data.
650 - */
651 - public static function getServerStatusData($host_id, $days = 7)
652 - {
653 - $period = WebTotem::getPeriod($days);
654 - $payload = '{ "query":"query($id: ID!, $dateRange: DateRangeInput!) { auth { viewer { sites { one(id: $id) { serverStatus { info { phpVersion phpServerUser phpServerSoftware phpGatewayInterface phpServerProtocol osInfo cpuCount cpuModel CpuFreq cpuFamily lsCpu maxExecTime mathLibraries } ramChart(dateRange: $dateRange){ total value time } cpuChart(dateRange: $dateRange){ value time } } } } } } }", "variables":{"id":"' . $host_id . '","dateRange":{"to":' . $period['to'] . ',"from":' . $period['from'] . '} } }';
655 -
656 - $response = self::sendRequest($payload, TRUE);
657 -
658 - if (isset($response['data']['auth']['viewer']['sites']['one']['serverStatus'])) {
659 - return $response['data']['auth']['viewer']['sites']['one']['serverStatus'];
660 - }
661 -
662 - return [];
663 - }
664 -
665 - /**
666 - * Method to remove port from ignore list.
667 - *
668 - * @param string $host_id
669 - * Host id on WebTotem.
670 - * @param string $port
671 - * User specified port.
672 - *
673 - * @return array
674 - * Returns information whether the request was successful.
675 - */
676 - public static function removeIgnorePort($host_id, $port)
677 - {
678 - $payload = '{"variables":{ "input": { "siteId": "' . $host_id . '", "port":' . $port . '} },"query":"mutation($input: IgnorePortInput!) { auth { sites { ps { removeIgnorePort(input: $input) } } } }"} ';
679 - return self::sendRequest($payload, TRUE);
680 - }
681 -
682 - /**
683 - * Method to add port to ignore list.
684 - *
685 - * @param string $host_id
686 - * Host id on WebTotem.
687 - * @param string $port
688 - * User specified port.
689 - *
690 - * @return array
691 - * Returns information whether the request was successful.
692 - */
693 - public static function addIgnorePort($host_id, $port)
694 - {
695 - $payload = '{"variables":{ "input": { "siteId": "' . $host_id . '", "port":' . (int)$port . '} },"query":"mutation($input: IgnorePortInput!) { auth { sites { ps { addIgnorePort(input: $input) } } } }"} ';
696 - return self::sendRequest($payload, TRUE);
697 - }
698 -
699 - /**
700 - * Method to get all ports list.
701 - *
702 - * @param string $host_id
703 - * Host id on WebTotem.
704 - *
705 - * @return array
706 - * Returns ports data.
707 - */
708 - public static function getAllPortsList($host_id)
709 - {
710 - $payload = '{"query":"query($id: ID!) { auth { viewer { sites { one(id: $id) { ports { status lastTest { time } ignorePorts TCPResults{ port technology version cveList{id summary } } UDPResults { port technology version cveList{id summary } } } } } } } } ","variables":{"id":"' . $host_id . '"}}';
711 -
712 - $response = self::sendRequest($payload, TRUE);
713 -
714 - if (isset($response['data']['auth']['viewer']['sites']['one']['ports'])) {
715 - return $response['data']['auth']['viewer']['sites']['one']['ports'];
716 - }
717 -
718 - return [];
719 - }
720 -
721 - /**
722 - * Method to get all ports list.
723 - *
724 - * @param string $host_id
725 - * Host id on WebTotem.
726 - *
727 - * @return array
728 - * Returns ports data.
729 - */
730 - public static function getOpenPaths($host_id)
731 - {
732 - $payload = '{"query":"query($id: ID!) { auth { viewer { sites { one(id: $id) { openPathSearch { time paths { httpCode severity path } } } } } } } ","variables":{"id":"' . $host_id . '"}}';
733 -
734 - $response = self::sendRequest($payload, TRUE);
735 -
736 - if (isset($response['data']['auth']['viewer']['sites']['one']['openPathSearch'])) {
737 - return $response['data']['auth']['viewer']['sites']['one']['openPathSearch'];
738 - }
739 -
740 - return [];
741 - }
742 -
743 - /**
744 - * Method to get all reports.
745 - *
746 - * @param string $host_id
747 - * Host id on WebTotem.
748 - * @param int $limit
749 - * Limit on the number of records.
750 - * @param string $cursor
751 - * Mark for loading data.
752 - *
753 - * @return array
754 - * Returns reports data.
755 - */
756 - public static function getAllReports($host_id, $limit = 10, $cursor = NULL)
757 - {
758 - $cursor = ($cursor == NULL) ? 'null' : '"' . $cursor . '"';
759 - $payload = '{"variables":{"filter": { "order": { "direction": "DESC", "field": "created_at"}, "siteId":"' . $host_id . '", "pagination":{"first":' . $limit . ', "cursor":' . $cursor . '} } },"query":"query ReportsQuery($filter: ReportListFilter!) { auth { viewer { reports { list(filter: $filter) { edges { node { id site { hostname } createdAt wa dc ps rc sc av waf } cursor } pageInfo { endCursor hasNextPage } } } } } }"}';
760 - $response = self::sendRequest($payload, TRUE);
761 -
762 - if (isset($response['data']['auth']['viewer']['reports']['list']['edges'])) {
763 - return $response['data']['auth']['viewer']['reports']['list'];
764 - }
765 -
766 - return [];
767 - }
768 -
769 - /**
770 - * Method to generate report.
771 - *
772 - * @param string $host_id
773 - * Host id on WebTotem.
774 - * @param int|array $days
775 - * For what period data is needed.
776 - * @param array $services
777 - * User-specified module settings.
778 - *
779 - * @return string|bool
780 - * Returns report download link.
781 - */
782 - public static function generateReport(string $host_id, $days, array $services)
783 - {
784 - $period = WebTotem::getPeriod($days);
785 - $language = WebTotem::getLanguage();
786 -
787 - $payload = '{"query":"query ($input: GenerateReportInput) { auth { viewer { reports { generate(input: $input) } } } }", "variables":{ "input": { "siteId": "' . $host_id . '", "from": ' . $period['from'] . ', "to": ' . $period['to'] . ', "wa": ' . $services['wa'] . ', "dc": ' . $services['dc'] . ', "ps": ' . $services['ps'] . ', "rc": ' . $services['rc'] . ', "sc": ' . $services['sc'] . ', "av": ' . $services['av'] . ', "waf": ' . $services['waf'] . ', "language": "' . $language . '" } } }';
788 - $response = self::sendRequest($payload, TRUE);
789 -
790 - if (isset($response['data']['auth']['viewer']['reports']['generate'])) {
791 - return $response['data']['auth']['viewer']['reports']['generate'];
792 - }
793 -
794 - return FALSE;
795 - }
796 -
797 - /**
798 - * Method to download report.
799 - *
800 - * @param string $id
801 - * Assigned to the report.
802 - *
803 - * @return string|bool
804 - * Returns report download link.
805 - */
806 - public static function downloadReport($id)
807 - {
808 - $payload = '{"query": "query { auth { viewer { reports { download(id: \"' . $id . '\") } } } }"}';
809 - $response = self::sendRequest($payload, TRUE);
810 -
811 - if (isset($response['data']['auth']['viewer']['reports']['download'])) {
812 - return $response['data']['auth']['viewer']['reports']['download'];
813 - }
814 -
815 - return FALSE;
816 - }
817 -
818 - /**
819 - * Method to get configs data.
820 - *
821 - * @param string $host_id
822 - * Host id on WebTotem.
823 - *
824 - * @return array|bool
825 - * Returns configs data.
826 - */
827 - public static function getConfigs($host_id)
828 - {
829 - $payload = '{"query":"query{ auth{ viewer{ sites{ one(id:\"' . $host_id . '\"){ configs{ ... on WaConfig { id service isActive notifications } ... on WafConfig { id service isActive notifications } ... on AvConfig { id service isActive notifications } ... on DcConfig { id service isActive notifications } ... on DecConfig { id service isActive } ... on RcConfig { id service isActive notifications} ... on CmsConfig { id service isActive } ... on PsConfig { id service isActive notifications } ... on SsConfig { id service isActive } ... on ScConfig { id service isActive } } } } } } } "}';
830 - $response = self::sendRequest($payload, TRUE);
831 -
832 - if (isset($response['data']['auth']['viewer']['sites']['one']['configs'])) {
833 - return $response['data']['auth']['viewer']['sites']['one']['configs'];
834 - }
835 -
836 - return FALSE;
837 - }
838 -
839 - /**
840 - * Method to toggle modules config.
841 - *
842 - * @param string $service_id
843 - * Service id that we enable or disable.
844 - *
845 - * @return string|bool
846 - * Returns information whether the request was successful.
847 - */
848 - public static function toggleConfigs($service_id)
849 - {
850 - $payload = '{"query":"mutation{ auth{ configs{ toggle(id: \"' . $service_id . '\"){ ... on WaConfig { service isActive } ... on AvConfig { service isActive } ... on DcConfig { service isActive } ... on DecConfig { service isActive } ... on RcConfig { service isActive } ... on CmsConfig { service isActive } ... on PsConfig { service isActive } ... on WafConfig { service isActive } } } } } "}';
851 - $response = self::sendRequest($payload, TRUE);
852 -
853 - if (isset($response['data']['auth']['configs']['toggle'])) {
854 - return $response['data']['auth']['configs']['toggle'];
855 - }
856 -
857 - return FALSE;
858 - }
859 -
860 - /**
861 - * Method to toggle modules notification.
862 - *
863 - * @param string $host_id
864 - * Host id on WebTotem.
865 - * @param string $service
866 - * Service id in which we enable or disable notifications.
867 - *
868 - * @return string|bool
869 - * Returns information whether the request was successful.
870 - */
871 - public static function toggleNotifications($host_id, $service)
872 - {
873 - $payload = '{"query":"mutation{ auth{ sites{ toggleNotifications(siteId: \"' . $host_id . '\", service: ' . $service . ') } } }"}';
874 - $response = self::sendRequest($payload, TRUE);
875 -
876 - if (isset($response['data']['auth']['sites']['toggleNotifications'])) {
877 - return $response;//['data']['auth']['sites']['toggleNotifications'];
878 - }
879 -
880 - return FALSE;
881 - }
882 -
883 - /**
884 736 * Method to get allow/deny ip list.
885 737 *
886 738 * @param string $type
887 739 * Type of ip list
@@ -945,231 +797,8 @@
945 797 return true;
946 798 }
947 799
948 800 /**
949 - * Method to get allow url list.
950 - *
951 - * @param string $host_id
952 - * Host id on WebTotem.
953 - *
954 - * @return array
955 - * Returns url allow lists.
956 - */
957 - public static function getAllowUrlList($host_id)
958 - {
959 - $payload = '{"query":"query { auth { viewer { sites { one(id: \"' . $host_id . '\"){ firewall{ urlWhiteList{ id url createdAt } } } } } } }"} ';
960 - $response = self::sendRequest($payload, TRUE);
961 -
962 - if (isset($response['data']['auth']['viewer']['sites']['one']['firewall']['urlWhiteList'])) {
963 - return $response['data']['auth']['viewer']['sites']['one']['firewall']['urlWhiteList'];
964 - }
965 -
966 - return [];
967 - }
968 -
969 - /**
970 - * Method to add url to allow list.
971 - *
972 - * @param string $host_id
973 - * Host id on WebTotem.
974 - * @param string $url
975 - * User-specified url.
976 - *
977 - * @return bool|string
978 - * Returns information whether the request was successful.
979 - */
980 - public static function addUrlToAllowList($host_id, $url)
981 - {
982 - $payload = '{"variables":{ "input": { "siteId": "' . $host_id . '", "url": "' . $url . '" } }, "query":"mutation($input: WafUrlWhiteListInput!) { auth { sites { waf { addToUrlWhiteList(input: $input) } } } }"} ';
983 - $response = self::sendRequest($payload, TRUE);
984 -
985 - if (isset($response['data']['auth']['sites']['waf']['addToUrlWhiteList'])) {
986 - return $response['data']['auth']['sites']['waf']['addToUrlWhiteList'];
987 - }
988 -
989 - return FALSE;
990 - }
991 -
992 - /**
993 - * Method to remove url from allow list.
994 - *
995 - * @param string $id
996 - * Id assignment to url address.
997 - *
998 - * @return bool|string
999 - * Returns information whether the request was successful.
1000 - */
1001 - public static function removeUrlFromAllowList($id)
1002 - {
1003 - $payload = '{"variables":{ "id": "' . $id . '" }, "query":"mutation($id: ID!) { auth { sites { waf { removeFromUrlWhiteList(id: $id) } } } }"} ';
1004 - $response = self::sendRequest($payload, TRUE);
1005 -
1006 - if (isset($response['data']['auth']['sites']['waf']['removeFromUrlWhiteList'])) {
1007 - return $response['data']['auth']['sites']['waf']['removeFromUrlWhiteList'];
1008 - }
1009 -
1010 - return FALSE;
1011 - }
1012 -
1013 - /**
1014 - * Method to get blocked countries list.
1015 - *
1016 - * @param string $host_id
1017 - * Host id on WebTotem.
1018 - *
1019 - * @return array
1020 - * Returns blocked countries list.
1021 - */
1022 - public static function getBlockedCountries($host_id)
1023 - {
1024 - $period = WebTotem::getPeriod(7);
1025 - $payload = '{"variables":{"dateRange":{"to":' . $period['to'] . ',"from":' . $period['from'] . '}} , "query":"query($dateRange: DateRangeInput!){ auth { viewer { sites { one(id: \"' . $host_id . '\"){ firewall{ blockedCountries map(dateRange: $dateRange) { attacks, country, location { country { nameEn } } } } } } } } }"}';
1026 - $response = self::sendRequest($payload, TRUE);
1027 -
1028 - if (isset($response['data']['auth']['viewer']['sites']['one']['firewall'])) {
1029 - return $response['data']['auth']['viewer']['sites']['one']['firewall'];
1030 - }
1031 -
1032 - return [];
1033 - }
1034 -
1035 - /**
1036 - * Method for synchronizing data on the list of blocked countries.
1037 - *
1038 - * @param string $host_id
1039 - * Host id on WebTotem.
1040 - * @param array $countries
1041 - * Array of countries to block.
1042 - *
1043 - * @return bool|string
1044 - * Returns information whether the request was successful.
1045 - */
1046 - public static function syncBlockedCountries($host_id, $countries)
1047 - {
1048 -
1049 - $countries = $countries ? WebTotem::convertArrayToString($countries) : '';
1050 - $payload = '{"variables":{ "input": { "siteId": "' . $host_id . '", "countries": [' . $countries . '] } }, "query":"mutation($input: WafBlockedCountriesInput!) { auth { sites { waf { syncBlockedCountries(input: $input) } } } }"} ';
1051 - $response = self::sendRequest($payload, TRUE);
1052 -
1053 - if (isset($response['data']['auth']['sites']['waf']['syncBlockedCountries'])) {
1054 - return $response['data']['auth']['sites']['waf']['syncBlockedCountries'];
1055 - }
1056 -
1057 - return FALSE;
1058 - }
1059 -
1060 - /**
1061 - * Method to get user's email.
1062 - *
1063 - * @return string
1064 - * Returns user's email.
1065 - */
1066 - public static function getEmail()
1067 - {
1068 - $payload = '{"query":"query { auth { viewer { email } } }"}';
1069 - $response = self::sendRequest($payload, true);
1070 -
1071 - return $response['data']['auth']['viewer']['email'];
1072 - }
1073 -
1074 - /**
1075 - * Method to get user's email.
1076 - *
1077 - * @param string $plugin_list
1078 - * List of plugins and their versions.
1079 - *
1080 - * @return array
1081 - * Returns cve list.
1082 - */
1083 - public static function getCVE($plugin_list)
1084 - {
1085 - $payload = '{"variables":{ "params": [' . $plugin_list . '] }, "query":"query searchByTechnologyAndVersion($params: [SearchByTechnologyAndVersionInput!]) { auth { viewer { cve { searchByTechnologyAndVersion(params: $params) { cves { cve_id id summary published reference } technology version } } } } }"}';
1086 - $response = self::sendRequest($payload, true);
1087 -
1088 - if (isset($response['data']['auth']['viewer']['cve']['searchByTechnologyAndVersion'])) {
1089 - return $response['data']['auth']['viewer']['cve']['searchByTechnologyAndVersion'];
1090 - }
1091 -
1092 - return [];
1093 - }
1094 -
1095 - /**
1096 - * Method to get user's feedback.
1097 - *
1098 - * @return array
1099 - */
1100 - public static function getFeedback()
1101 - {
1102 - return self::sendFeedbackRequest("GET");
1103 - }
1104 -
1105 - /**
1106 - * Method to set user's feedback.
1107 - *
1108 - * @return array
1109 - */
1110 - public static function setFeedback($data)
1111 - {
1112 - return self::sendFeedbackRequest("POST", $data);
1113 - }
1114 -
1115 - /**
1116 - * Function sends data request to endpoint.
1117 - *
1118 - * @param array $data
1119 - * Data array to be sent to endpoint.
1120 - *
1121 - * @return array
1122 - * Returns response from WebTotem endpoint.
1123 - */
1124 - protected static function sendFeedbackRequest($method, $data = [])
1125 - {
1126 - $url = 'https://nps.wtotem.com/user-score';
1127 - $email = WebTotem::getUserEmail();
1128 -
1129 - if (!$email) {
1130 - return [];
1131 - }
1132 -
1133 - if ($method == "GET") {
1134 -
1135 - $args = [
1136 - 'timeout' => '30',
1137 - 'sslverify' => FALSE,
1138 - ];
1139 -
1140 - $response = wp_remote_get($url . '?email=' . urlencode($email), $args);
1141 -
1142 - } else {
1143 - $data['email'] = $email;
1144 - $data['platform'] = 'WORDPRESS';
1145 - $data = json_encode($data);
1146 -
1147 - $args = [
1148 - 'body' => $data,
1149 - 'timeout' => '30',
1150 - 'sslverify' => FALSE,
1151 - 'headers' => [
1152 - 'Content-Type' => 'application/json',
1153 - ],
1154 - ];
1155 -
1156 - $response = wp_remote_post($url, $args);
1157 - }
1158 -
1159 -
1160 - $http_code = wp_remote_retrieve_response_code($response);
1161 -
1162 - if ($http_code < 200) {
1163 - // WebTotemOption::setNotification('error', __('Could not connect to feedback endpoint.', 'wtotem'));
1164 - return [];
1165 - }
1166 -
1167 - $response_body = wp_remote_retrieve_body($response);
1168 - return json_decode($response_body, true);
1169 - }
1170 -
1171 - /**
1172 801 * Sends a REST API request to the WebTotem API server.
1173 802 *
1174 803 * @param string $endpoint
1175 804 * REST API endpoint (e.g., 'scan', 'status', etc.).
@@ -1184,10 +813,11 @@
1184 813 *
1185 814 * @return array|null
1186 815 * API response as an associative array, or null on failure.
1187 816 */
1188 - protected static function sendRequest($endpoint, $data = [], $method = 'POST', $useToken = false, $retry = false)
817 + protected static function sendRequest($endpoint, $data = [], $method = 'POST', $useToken = false, $retry = false, $silent = false)
1189 818 {
819 + self::$last_status = 0;
1190 820 $api_key = WebTotemOption::getOption('api_key');
1191 821
1192 822 // Get or initialize the API URL.
1193 823 $api_url = WebTotemOption::getOption('api_url');
@@ -1195,21 +825,30 @@
1195 825 $api_url = self::getApiUrl();
1196 826 WebTotemOption::setOptions(['api_url' => $api_url]);
1197 827 }
1198 828
1199 - $auth_token = null;
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;
1200 843 if ($useToken) {
1201 844 $auth_token = WebTotemOption::getOption('auth_token');
1202 - $auth_token_expired = WebTotemOption::getOption('auth_token_expired');
845 + $auth_token_expired = (int) WebTotemOption::getOption('auth_token_expired');
1203 846
1204 - if ($auth_token_expired <= time() && !$retry) {
847 + if ((!$auth_token || $auth_token_expired <= time()) && !$retry) {
1205 848 $result = self::auth($api_key);
1206 849 if ($result === 'success') {
1207 - return self::sendRequest($endpoint, $data, $method, $useToken, true);
1208 - } elseif (isset($result['message'])) {
1209 - $message = WebTotem::messageForHuman($result['message']);
1210 -// WebTotemOption::setNotification('info', '$endpoint: ' . $endpoint);
1211 - WebTotemOption::setNotification('error', $message);
850 + return self::sendRequest($endpoint, $data, $method, $useToken, TRUE, $silent);
1212 851 }
1213 852 }
1214 853 }
1215 854
@@ -1216,9 +855,9 @@
1216 855 $url = rtrim($api_url, '/') . '/api/v1/' . ltrim($endpoint, '/');
1217 856
1218 857 $args = [
1219 858 'timeout' => 60,
1220 - 'sslverify' => false,
859 + 'sslverify' => TRUE,
1221 860 'headers' => [
1222 861 'Accept' => 'application/json',
1223 862 'Content-Type' => 'application/json',
1224 863 'source' => 'WORDPRESS',
@@ -1231,57 +870,146 @@
1231 870
1232 871 if (strtoupper($method) === 'GET') {
1233 872 $url = add_query_arg($data, $url);
1234 873 } else {
1235 - $args['body'] = json_encode($data);
874 + $args['body'] = wp_json_encode($data);
1236 875 }
1237 876
1238 877 $response = wp_remote_request($url, array_merge($args, ['method' => strtoupper($method)]));
1239 878
1240 - $errors = ['status' => false];
1241 879 if (is_wp_error($response)) {
1242 - $errors = [
1243 - 'status' => true,
1244 - 'message' => 'SERVER UNAVAILABLE: ' . $response->get_error_message(),
1245 - ];
880 + self::notify($silent, 'error', WebTotem::messageForHuman(
881 + 'SERVER UNAVAILABLE: ' . $response->get_error_message()
882 + ));
883 +
884 + return NULL;
1246 885 }
1247 886
887 + $code = (int) wp_remote_retrieve_response_code($response);
888 + self::$last_status = $code;
1248 889 $body = wp_remote_retrieve_body($response);
1249 - $decoded = json_decode($body, true);
890 + $decoded = json_decode($body, TRUE);
1250 891
1251 - if (isset($decoded['message']) or $errors['status']) {
1252 - $errorMessage = $errors['status'] ? $errors['message'] : $decoded['message'];
892 + if (!is_array($decoded)) {
893 + $decoded = [];
894 + }
1253 895
1254 - if (stripos($errorMessage, "Password expired") !== false) {
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) {
1255 901 wtotem_error_page(['errors' => 'PASSWORD_EXPIRED']);
1256 902 exit();
1257 - } elseif (stripos($errorMessage, "API_KEY_DEACTIVATED") !== false) {
903 + }
904 +
905 + if (stripos($error_message, 'API_KEY_DEACTIVATED') !== FALSE) {
1258 906 wtotem_error_page(['errors' => 'TARIFF_EXPIRED']);
1259 907 exit();
1260 908 }
909 + }
1261 910
1262 - $message = WebTotem::messageForHuman($errorMessage);
1263 - if ($errorMessage == "invalid credentials" && !$retry) {
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 + ));
1264 920
1265 - if (self::auth($api_key) === 'success') {
921 + return NULL;
922 + }
1266 923
1267 - return self::sendRequest($endpoint, $data, $method, $useToken, true);
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);
1268 955 }
1269 - } elseif (stripos($errorMessage, "USERHOST_NOT_BELONG_TO_USER") !== false) {
1270 - if (WebTotem::isMultiSite()) {
1271 - WebTotemOption::clearAllHosts();
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();
1272 989 }
1273 - WebTotemOption::clearOptions(['host_id', 'host_name']);
1274 - } else {
1275 -// WebTotemOption::setNotification('info', '$endpoint: ' . $endpoint);
1276 - WebTotemOption::setNotification('error', $message);
1277 990 }
1278 991 }
1279 992
1280 -// if (empty($decoded)) {
1281 -// self::checkEndpoint();
1282 -// }
993 + if ($delay < 1) {
994 + $delay = 60;
995 + }
1283 996
1284 - return $decoded;
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']);
1285 1013 }
1286 1014
1287 1015 }