# wt-security/trunk/lib/API.php

WebTotem Security, version trunk. 1,016 lines.

- Page: https://pluginprobe.com/plugins/wt-security/trunk/code/lib/API.php
- Raw: https://pluginprobe.com/plugins/wt-security/trunk/raw/lib/API.php
- Modified: 2026-09-14T08:28:04+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/wt-security/trunk/code/lib/API.php#L10-L20`.

```php
<?php

if (!defined('WEBTOTEM_INIT') || WEBTOTEM_INIT !== true) {
    if (!headers_sent()) {
        header('HTTP/1.1 403 Forbidden');
    }
    die("Protected By WebTotem!");
}

/**
 * WebTotem API class.
 *
 * Mostly contains wrappers for API methods. Check and send methods.
 *
 * @version 1.0
 * @copyright (C) 2022 WebTotem team (http://wtotem.com)
 * @license GNU/GPL: http://www.gnu.org/copyleft/gpl.html
 */
class WebTotemAPI extends WebTotem
{

    /**
     * HTTP status of the most recent API call, 0 when the request never landed.
     *
     * @var int
     */
    protected static $last_status = 0;

    /**
     * HTTP status of the most recent API call.
     *
     * @return int
     *   Status code, or 0 when the request did not reach the server.
     */
    public static function getLastStatus()
    {
        return (int) self::$last_status;
    }

    /**
     * Raises a notification unless the caller asked to stay quiet.
     *
     * Probing calls (such as the WebSocket ticket, which is expected to be
     * missing on older API builds) must not spam the admin with errors.
     *
     * @param bool $silent
     *   TRUE to swallow the notification.
     * @param string $type
     *   Notification type.
     * @param string $message
     *   Notification text.
     *
     * @return void
     */
    protected static function notify($silent, $type, $message)
    {
        if (!$silent) {
            WebTotemOption::setNotification($type, $message);
        }
    }


    /**
     * Method for getting an auth token.
     *
     * @param string $api_key
     *   Application programming interface key.
     *
     * @return bool|string
     *   Returns auth status
     */
    public static function auth($api_key, $repeat = FALSE)
    {
        $domain = WEBTOTEM_SITE_DOMAIN;

        if (empty($api_key)) {
            return FALSE;
        }

        $data = ['api_key' => $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']);
    }

}

```
