$api_key, 'site' => $domain]; $result = self::sendRequest('auth/sign-in/api-key', $data, 'POST', FALSE, TRUE); if($result === null){ WebTotemOption::setNotification('warning' , __('Authorization failed. The server may be temporarily unavailable', 'wtotem')); } if (isset($result['access_token'])) { $auth_token = $result['access_token']; if(!WebTotemOption::isActivated()){ WebTotemOption::login(['token' => $auth_token, 'api_key' => $api_key]); WebTotemAgentManager::postdelete(); } else { WebTotemOption::refreshToken($auth_token); } return 'success'; } elseif (isset($result['message']) and $result['message'] == 'invalid credentials') { WebTotemOption::logout(); } if($repeat == false){ //self::checkEndpoint(); return self::auth($api_key, true); } return FALSE; } /** * Method for getting API url. * * @return string|bool * API url */ public static function getApiUrl() { return 'https://app.wtotem.com'; } /** * Method for getting the WebSocket endpoint url. * * @return string * WebSocket url, without any credentials. */ public static function getWsUrl() { $api_url = WebTotemOption::getOption('api_url'); if (!$api_url) { $api_url = self::getApiUrl(); } return preg_replace('#^http#i', 'ws', rtrim($api_url, '/')) . '/api/v1/ws'; } /** * Get site info from API server. * * @param string $attempt * Is the request an attempt to get host data. * * @return array * Returns host data. */ public static function siteInfo($attempt = FALSE) { if (self::isMultiSite()) { $host['id'] = WebTotemOption::getSessionOption('host_id'); $host['name'] = WebTotemOption::getSessionOption('host_name'); if ($host['id']) { return $host; } } $host = WebTotemOption::getHost(); if ($host['id']) { return $host; } // if (self::isMultiSite()) { // $sites = get_sites(); // foreach ($sites as $site) { // $domain = untrailingslashit($site->domain . $site->path); // self::addSite($domain); // } // // if (!$attempt) { // return self::siteInfo(TRUE); // } // } else { // $domain = WEBTOTEM_SITE_DOMAIN; // return self::addSite($domain); // } $domain = WEBTOTEM_SITE_DOMAIN; return self::addSite($domain); // return []; } /** * Method for adding a site to the WebTotem platform. * * @param string $domain * Domain to add. * * @return array * Returns host data. */ public static function addSite($domain) { if (function_exists('idn_to_utf8')) { $domain = idn_to_utf8($domain); } // Checking if the site has been added to the WebTotem. if(!$host = self::getHostID($domain)){ $host = self::getHostID('www.' . $domain); } if($host['id']){ // Remember the binding: otherwise every page load asks the API // for the host id again (and again for the www. variant). WebTotemOption::setHost($host['hostname'], $host['id']); return [ 'id' => $host['id'], 'name' => $host['hostname'], ]; } // If the site is not added then try to add. $data = ['hosts' => [$domain]]; $response = self::sendRequest('hosts', $data, 'POST', TRUE); if (isset($response['message'])) { WebTotemOption::setNotification('error', __('Failed to add the site to the WebTotem platform.', 'wtotem')); } else { if ($response['data']['added']) { // If it added, save site ID. $host = self::getHostID($domain); WebTotemOption::setHost($domain, $host['id']); return [ 'id' => $host['id'], 'name' => $host['hostname'], ]; } } return []; } /** * Get all sites from API. * * @param string $page_num * Mark for loading data. * @param string $limit * Limit of sites to loading. * * @return array * Returns host data. */ public static function getSites($page_num = 1, $page_size = 15, $status = 'active', $hostname = '') { $query = [ 'page_num' => $page_num, 'page_size' => $page_size, 'status' => $status, ]; if ($hostname !== '') { $query['hostname'] = $hostname; } $result = self::sendRequest('hosts', $query, 'GET', TRUE); // The API answers { "data": { "hosts": [...], "host_limit": n, "can_defrost": bool } }. $payload = self::payload($result); return isset($payload['hosts']) && is_array($payload['hosts']) ? $payload['hosts'] : []; } /** * Reads the payload out of the API response envelope. * * Most endpoints answer { "data": ... }; a few older builds used "Data". * * @param mixed $response * Decoded API response. * * @return array * Payload, or an empty array when the response carried none. */ protected static function payload($response) { if (!is_array($response)) { return []; } foreach (['data', 'Data'] as $key) { if (isset($response[$key]) && is_array($response[$key])) { return $response[$key]; } } return []; } /** * Requests a single-use ticket for the browser WebSocket connection. * * The ticket replaces the access token that used to be printed into the * page: it is short-lived, may be used once, and is bound to the browser * Origin it was issued for. * * @param string $origin * Browser origin the ticket is issued for, e.g. https://example.com. * * @return string * The ticket, or an empty string when it could not be obtained. */ public static function getWSTicket($origin = '') { $query = []; if (is_string($origin) && $origin !== '') { // add_query_arg() does not encode values; the origin carries "://". $query['origin'] = rawurlencode($origin); } $response = self::sendRequest('ws/ticket', $query, 'GET', TRUE, FALSE, TRUE); if (is_array($response) && !empty($response['ticket'])) { return (string) $response['ticket']; } // Tolerate a wrapped answer in case the endpoint starts using the envelope. $payload = self::payload($response); return !empty($payload['ticket']) ? (string) $payload['ticket'] : ''; } /** * Check the site's presence in the list on the API side. * * @param string $site * The domain we want to check. * * @return array * Returns host data. */ public static function getHostID($site) { $result = self::sendRequest('hosts/id', ['hostname' => $site], 'GET', TRUE); if (isset($result['data'])) { WebTotemOption::setOptions(['config_id' => $result['data']['config_id']]); return ['id' => $result['data']['host_id'], 'hostname' => $site]; } return ['id' => '', 'hostname' => '']; } /** * Method to get the agents file names and AM file link. * * @param string $host_id * Host id on WebTotem. * * @return array * Returns agents files data. */ public static function getAgentsFiles($host_id) { // if (WebTotem::isMultiSite()) { // $all_hosts = WebTotemOption::getOption('all_hosts'); // $all_hosts = $all_hosts ? json_decode($all_hosts, true) : []; // // $siteIdsArray = $all_hosts ? array_values($all_hosts) : []; // $siteIds = $siteIdsArray ? addslashes(WebTotem::convertArrayToString($siteIdsArray)) : ''; // // $payload = '{"query":"mutation { auth { am { installMultisite(mainSiteId: \"' . $host_id . '\", siteIds: [' . $siteIds . ']){ downloadLink, amFilename, wafFilename, avFilename } } } }"}'; // $response = self::sendRequest($payload, TRUE); // // if (isset($response['data']['auth']['am']['installMultisite'])) { // return $response['data']['auth']['am']['installMultisite']; // } // } else { $response = self::sendRequest('/agents/' . $host_id . '/install', [], 'POST', TRUE); if (isset($response['data'])) { return $response['data']; } // } return []; } /** * Add secondary MultiSite host. * * @param $new_sites * An array with sites to add. * * @return void. */ public static function addMultiSiteNewSites($new_sites) { } /** * Get the date of creation of the site. * * @param string $site * The domain we want to check. * * @return string|bool * Returns host data. */ public static function getGetSiteAddedDate($site) { if (!$site) { return FALSE; } $hosts = self::getSites(1, 1, 'active', $site); foreach ($hosts as $host) { if (!empty($host['created_at'])) { return $host['created_at']; } } return FALSE; } /** * Remove secondary MultiSite host. * * @param $host_id * Host id on WebTotem. * * @return bool * Returns result removing host. */ public static function removeMultiSiteHost($host_id) { return false; } /** * Method to get agents (AM, WAF, AV) statuses. * * @return array * Returns agents statuses data. */ public static function getAgentsStatusesFromAPI() { $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/agents/' . $config_id . '/status', [], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method for get monitoring data. * * @param string $host_id * Host id on WebTotem. * @param int|array $days * For what period data is needed. * * @return array * Returns all data. */ public static function getMonitoringData($host_id) { $response = self::sendRequest('/dashboard/monitoring/' . $host_id . '/results', [], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to get firewall data. * * @param int $limit * Limit on the number of records. * @param string $page * Page for loading data. * @param int|array $days * For what period data is needed. * * @return array * Returns firewall data. */ public static function getFirewall($limit = 20, $page = 1, $days = 365) { $period = WebTotem::getPeriod($days); $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/logs', [ 'page_num' => $page, 'page_size' => $limit, 'from' => $period['from'], 'to' => $period['to'] ], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to get firewall chart data. * * @param int $days * For what period data is needed. * * @return array * Returns firewall chart data. */ public static function getFirewallStatistics($days = 7) { $period = WebTotem::getPeriod($days); $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/statistics', [ 'from' => $period['from'], 'to' => $period['to'] ], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to get firewall settings. * * @return array * Returns information whether the request was successful. */ public static function getFirewallSettings() { $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/configs', [], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to set firewall settings. * * @param array $settings * User-specified settings. * * @return array * Returns information whether the request was successful. */ public static function setFirewallSettings(array $settings) { $config_id = WebTotemOption::getOption('config_id'); return self::sendRequest('/dashboard/firewall/' . $config_id . '/configs', $settings, 'PATCH', TRUE); } /** * Method to get antivirus history data. * * @param int $page_num * Page number. * @param int $page_size * Number of entries per page. * * @return array * Returns antivirus history data. */ public static function getAntivirusHistory($page_num = 1, $page_size = 10) { $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/history', [ 'page_num' => $page_num, 'page_size' => $page_size ], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to get antivirus history details data. * * @param int $scan_id * Scan ID. * @param int $page_num * Page number. * @param int $page_size * Number of entries per page. * * @return array * Returns antivirus history data. */ public static function getAntivirusHistoryDetails($scan_id, $page_num = 1, $page_size = 10) { $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/history/' . $scan_id . '/details', [ 'page_num' => $page_num, 'page_size' => $page_size ], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to get quarantine data. * * @param int $page_num * Page number. * @param int $page_size * Number of entries per page. * * @return array * Returns quarantine data. */ public static function getAntivirusCurrentDetails($page_num = 1, $page_size = 5) { $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/current/details', [ 'page_num' => $page_num, 'page_size' => $page_size ], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to force check Antivirus. * * @return mixed * Returns information whether the request was successful. */ public static function forceCheckAV() { $config_id = WebTotemOption::getOption('config_id'); return self::sendRequest('/dashboard/antivirus/' . $config_id . '/check', [], 'POST', TRUE); } /** * Method to force check services. * * @param string $host_id * Host id on WebTotem. * @param string $module_name * Service that needs to be checked. * * @return array * Returns information whether the request was successful. */ public static function forceCheck($host_id, $module_name) { return self::sendRequest('/dashboard/hosts/' . $host_id . '/check', ['module_name' => $module_name], 'POST', TRUE); } /** * Method to get quarantine data. * * @param int $page_num * Page number. * @param int $page_size * Number of entries per page. * * @return array * Returns quarantine data. */ public static function getQuarantineList($page_num = 1, $page_size = 5) { $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine', [ 'page_num' => $page_num, 'page_size' => $page_size ], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to move file to quarantine. * * @param string $file_id * File ID. * * @return array * Returns information whether the request was successful. */ public static function moveToQuarantine($file_id) { $config_id = WebTotemOption::getOption('config_id'); return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/to-quarantine', [ 'file_id' => $file_id, ], 'POST', TRUE); } /** * Method to move file from quarantine. * * @param string $file_id * File ID. * * @return array * Returns information whether the request was successful. */ public static function moveFromQuarantine($file_id) { $config_id = WebTotemOption::getOption('config_id'); return self::sendRequest('/dashboard/antivirus/' . $config_id . '/quarantine/from-quarantine', [ 'file_id' => $file_id, ], 'POST', TRUE); } /** * Method to get allow/deny ip list. * * @param string $type * Type of ip list * * @return array|bool * Returns ip allow/deny lists. */ public static function getIpLists($type = 'blacklist') { $config_id = WebTotemOption::getOption('config_id'); $response = self::sendRequest('/dashboard/firewall/' . $config_id . '/configs/iplist', [ 'type' => $type, ], 'GET', TRUE); if (isset($response['data'])) { return $response['data']; } return []; } /** * Method to add ip to allow/deny list. * * @param string $ip * Ip address. * @param string $type * Allow or deny type. * * @return bool * Returns information whether the request was successful. */ public static function addIpToList($ips, $type) { $config_id = WebTotemOption::getOption('config_id'); self::sendRequest('/dashboard/firewall/' . $config_id . '/configs/iplist?type=' . $type, [ 'ip' => array_filter($ips), ], 'POST', TRUE); return true; } /** * Method to remove ip from allow/deny list by id. * * @param string $ip * Ip address. * @param string $type * Allow or deny type. * * @return bool * Returns information whether the request was successful. */ public static function removeIpFromList($ip, $type) { $config_id = WebTotemOption::getOption('config_id'); self::sendRequest('/dashboard/firewall/' . $config_id . '/configs/iplist?type=' . $type, [ 'ip' => $ip, ], 'DELETE', TRUE); return true; } /** * Sends a REST API request to the WebTotem API server. * * @param string $endpoint * REST API endpoint (e.g., 'scan', 'status', etc.). * @param array $data * Associative array of data to send as JSON body or query parameters. * @param string $method * HTTP method: GET, POST, PUT, DELETE (default is POST). * @param bool $useToken * Whether to include the auth token. * @param bool $retry * Used to prevent recursion on token renewal. * * @return array|null * API response as an associative array, or null on failure. */ protected static function sendRequest($endpoint, $data = [], $method = 'POST', $useToken = false, $retry = false, $silent = false) { self::$last_status = 0; $api_key = WebTotemOption::getOption('api_key'); // Get or initialize the API URL. $api_url = WebTotemOption::getOption('api_url'); if (!$api_url) { $api_url = self::getApiUrl(); WebTotemOption::setOptions(['api_url' => $api_url]); } // The API previously answered 429: respect the requested pause instead of // hammering the server (and getting the whole site throttled). $throttled_until = (int) WebTotemOption::getOption('api_retry_after'); if ($throttled_until > time()) { self::notify($silent, 'warning', sprintf( /* translators: %d: number of seconds to wait. */ __('Too many requests to the WebTotem API. Please try again in %d seconds.', 'wtotem'), $throttled_until - time() )); return NULL; } $auth_token = NULL; if ($useToken) { $auth_token = WebTotemOption::getOption('auth_token'); $auth_token_expired = (int) WebTotemOption::getOption('auth_token_expired'); if ((!$auth_token || $auth_token_expired <= time()) && !$retry) { $result = self::auth($api_key); if ($result === 'success') { return self::sendRequest($endpoint, $data, $method, $useToken, TRUE, $silent); } } } $url = rtrim($api_url, '/') . '/api/v1/' . ltrim($endpoint, '/'); $args = [ 'timeout' => 60, 'sslverify' => TRUE, 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'source' => 'WORDPRESS', ], ]; if ($auth_token) { $args['headers']['Authorization'] = "Bearer $auth_token"; } if (strtoupper($method) === 'GET') { $url = add_query_arg($data, $url); } else { $args['body'] = wp_json_encode($data); } $response = wp_remote_request($url, array_merge($args, ['method' => strtoupper($method)])); if (is_wp_error($response)) { self::notify($silent, 'error', WebTotem::messageForHuman( 'SERVER UNAVAILABLE: ' . $response->get_error_message() )); return NULL; } $code = (int) wp_remote_retrieve_response_code($response); self::$last_status = $code; $body = wp_remote_retrieve_body($response); $decoded = json_decode($body, TRUE); if (!is_array($decoded)) { $decoded = []; } $error_message = isset($decoded['message']) ? (string) $decoded['message'] : ''; // Terminal account states: show the dedicated page and stop. if ($error_message !== '') { if (stripos($error_message, 'Password expired') !== FALSE) { wtotem_error_page(['errors' => 'PASSWORD_EXPIRED']); exit(); } if (stripos($error_message, 'API_KEY_DEACTIVATED') !== FALSE) { wtotem_error_page(['errors' => 'TARIFF_EXPIRED']); exit(); } } // 429 Too Many Requests: honour Retry-After and never re-authorize. if ($code === 429) { $delay = self::parseRetryAfter(wp_remote_retrieve_header($response, 'retry-after')); WebTotemOption::setOptions(['api_retry_after' => time() + $delay]); self::notify($silent, 'warning', sprintf( /* translators: %d: number of seconds to wait. */ __('Too many requests to the WebTotem API. Please try again in %d seconds.', 'wtotem'), $delay )); return NULL; } // 401 Unauthorized: the token is gone or expired. Re-login by API key once. if ($code === 401) { if (!$retry && $api_key && self::auth($api_key, TRUE) === 'success') { return self::sendRequest($endpoint, $data, $method, $useToken, TRUE, $silent); } self::notify($silent, 'error', WebTotem::messageForHuman( $error_message !== '' ? $error_message : 'invalid credentials' )); return $decoded; } // 403 Forbidden: authenticated, but not allowed. Re-login would not help. if ($code === 403) { if (stripos($error_message, 'USERHOST_NOT_BELONG_TO_USER') !== FALSE) { self::forgetHost(); } else { self::notify($silent, 'error', WebTotem::messageForHuman( $error_message !== '' ? $error_message : 'access denied' )); } return $decoded; } // Older API builds answer 200 with an error message in the body. if ($error_message !== '') { if ($error_message === 'invalid credentials') { if (!$retry && $api_key && self::auth($api_key, TRUE) === 'success') { return self::sendRequest($endpoint, $data, $method, $useToken, TRUE, $silent); } } elseif (stripos($error_message, 'USERHOST_NOT_BELONG_TO_USER') !== FALSE) { self::forgetHost(); } else { self::notify($silent, 'error', WebTotem::messageForHuman($error_message)); } } return $decoded; } /** * Reads the Retry-After header into a number of seconds. * * The header is either a number of seconds or an HTTP date. * * @param string $header * Raw Retry-After header value. * * @return int * Seconds to wait, clamped to a sane range. */ protected static function parseRetryAfter($header) { $delay = 0; $header = is_string($header) ? trim($header) : ''; if ($header !== '') { if (ctype_digit($header)) { $delay = (int) $header; } else { $timestamp = strtotime($header); if ($timestamp !== FALSE) { $delay = $timestamp - time(); } } } if ($delay < 1) { $delay = 60; } // Never park the plugin for longer than an hour. return min($delay, HOUR_IN_SECONDS); } /** * Drops the locally stored host binding after the API disowned it. * * @return void */ protected static function forgetHost() { if (WebTotem::isMultiSite()) { WebTotemOption::clearAllHosts(); } WebTotemOption::clearOptions(['host_id', 'host_name']); } }