<commit_before><?php
namespace Kawana;

class Client {
    const DEFAULT_PORT = 9291;

    const PASS = 0;
    const CAPTCHA = 1;
    const BLOCK = 2;

    protected $captchaThresholds;
    protected $blockThresholds;
    protected $forgivenThreshold;
    protected $readTimeout;
  
    public function __construct($hostname, $port = self::DEFAULT_PORT) {
        $this->address = $hostname;
        $this->port = $port;
        $this->readTimeout = null;
    }

    public function setCaptchaThresholds($fiveMin, $hour, $day) {
        $this->captchaThresholds = [];
        $this->setThresholds($this->captchaThresholds, $fiveMin, $hour, $day);
    }

    public function setBlockThresholds($fiveMin, $hour, $day) {
        $this->blockThresholds = [];
        $this->setThresholds($this->blockThresholds, $fiveMin, $hour, $day);
    }

    public function setForgivenThreshold($numForgiven) {
        if($numForgiven <= 0) {
            throw new \InvalidArgumentException("numForgiven cannot be <= 0");
        }
        $this->forgivenThreshold = $numForgiven;
    }

    /*
    *  Set the socket read timeout in milliseconds.
    *  @param $milliseconds must be < 1000 and > 0
    */
    public function setReadTimeout($milliseconds) {
        if($milliseconds >= 1000 || $milliseconds <= 0) {
            throw new \InvalidArgumentException("milliseconds must be < 1000 and > 0");
        }

        $this->readTimeout = array("sec" => 0, "usec" => $milliseconds * 1000);
    }

    /*
    * Add the $impactAmount to the fiveMin, hour, and day windows for $ip
    * @param $ip long|string
    * @param $impactAmount int
    * @return one of PASS, CAPTCHA, or BLOCK based on the updated data.
    *         always returns PASS if the $ip is whitelisted, else
    *         returns BLOCK if the $ip is blacklisted.
    */
    public function logIP($ip, $impactAmount) {
        $this->ensureThresholdsSet();

        // convert string like '127.0.0.1' to long
        if(is_string($ip)) { $ip = ip2long($ip); }

        if($impactAmount <= 0) {
            throw new \InvalidArgumentException("logIP impactAmount cannot be <= 0");
        }

        $cmd = 0x01; // kawana command byte for LogIP
        $bytes = pack("CVV", $cmd, $ip, $impactAmount);
        $resp = $this->sendAndRecv($bytes);
        return $this->checkIPData($resp);
    }

    /*
    * Subtract a percentage of the captcha threshold from the $ip's 
    * fiveMin, hour, and day windows.
    * @param $percent optional between 0.0 and 1.0, defaults to 0.5
    * @return one of PASS, CAPTCHA, or BLOCK based on the updated data.
    *         always returns PASS if the $ip is whitelisted, else
    *         returns BLOCK if the $ip is blacklisted.
    */
    public function forgiveIP($ip, $percent = 0.5) {
        $this->ensureThresholdsSet();

        // convert string like '127.0.0.1' to long
        if(is_string($ip)) { $ip = ip2long($ip); }

        if($percent <= 0.0 || $percent > 1.0) {
            throw new \InvalidArgumentException("forgiveIP percent cannot be <= 0.0 or > 1.0");
        }

        $fiveMin = $this->captchaThresholds['fiveMin'];
        $hour = $this->captchaThresholds['hour'];
        $day = $this->captchaThresholds['day'];

        $cmd = 0x02; // kawana command byte for ForgiveIP
        $bytes = pack("CVVVV", $cmd, $ip, $fiveMin, $hour, $day);
        $resp = $this->sendAndRecv($bytes);
        return $this->checkIPData($resp);
    }

    public function whitelistIP($ip) {
        $this->setBlackWhite($ip, 1);
    }

    public function unWhitelistIP($ip) {
        $this->setBlackWhite($ip, 2);
    }

    public function blacklistIP($ip) {
        $this->setBlackWhite($ip, 3);
    }

    public function unBlacklistIP($ip) {
        $this->setBlackWhite($ip, 4);
    }

    protected function setBlackWhite($ip, $modifier) {
        $validMods = [1, 2, 3, 4];
        if(!in_array($modifier, $validMods)) {
            throw new \InvalidArgumentException("Invalid BlackWhite modifier: $modifier");
        }

        // convert string like '127.0.0.1' to long
        if(is_string($ip)) { $ip = ip2long($ip); }

        $cmd = 0x03; // kawana command byte for BlackWhite
        $bytes = pack("CVC", $cmd, $ip, $modifier);
        $this->sendAndRecv($bytes);
    }

    protected function checkIPData($resp) {
        $result = unpack("V3impacts/Sforgiven/Cbw", $resp);

        foreach(['impacts1', 'impacts2', 'impacts3', 'forgiven', 'bw'] as $field) {
            if(!isset($result[$field])) {
                throw new \Exception("Invalid response from server");
            }
        }

        $maxImpacts = [
            'fiveMin' => $result['impacts1'],
            'hour'    => $result['impacts2'],
            'day'     => $result['impacts3']
        ];

        // check blacklist and whitelist
        if($result['bw'] & 0x01) {
            return self::PASS;
        }
        if($result['bw'] & 0x02) {
            return self::BLOCK;
        }

        // check block
        foreach($maxImpacts as $time => $impact) {
            if($impact >= $this->blockThresholds[$time]) {
                return self::BLOCK;
            }
        }

        // check captcha
        foreach($maxImpacts as $time => $impact) {
            if($impact >= $this->captchaThresholds[$time]) {
                return self::CAPTCHA;
            }
        }

        // check forgiven
        if($result['forgiven'] >= $this->forgivenThreshold) {
            return self::BLOCK;
        }

        return self::PASS;
    }

    /*
     * @return string response bytes
     */
    protected function sendAndRecv($bytes) {
        $responseLength = 15; // kawana response is 15 bytes

        // create socket
        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if($socket === false) {
            throwSocketException('socket_create', $socket);
        }

        if(null !== $this->readTimeout && !socket_set_option($socket,SOL_SOCKET, SO_RCVTIMEO, $this->readTimeout)) {
            throwSocketException('socket_set_options', $socket);
        }

        // connect to kawana server
        $connected = socket_connect($socket, $this->address, $this->port);
        if ($connected === false) {
            throwSocketException('socket_connect', $socket);
        } 

        // write bytes to server
        $numBytes = strlen($bytes);
        for($sent = 0; $sent < $numBytes;) {
            $n = socket_write($socket, $bytes);<commit_msg>suppress socket_connect errors<commit_after><?php
namespace Kawana;

class Client {
    const DEFAULT_PORT = 9291;

    const PASS = 0;
    const CAPTCHA = 1;
    const BLOCK = 2;

    protected $captchaThresholds;
    protected $blockThresholds;
    protected $forgivenThreshold;
    protected $readTimeout;
  
    public function __construct($hostname, $port = self::DEFAULT_PORT) {
        $this->address = $hostname;
        $this->port = $port;
        $this->readTimeout = null;
    }

    public function setCaptchaThresholds($fiveMin, $hour, $day) {
        $this->captchaThresholds = [];
        $this->setThresholds($this->captchaThresholds, $fiveMin, $hour, $day);
    }

    public function setBlockThresholds($fiveMin, $hour, $day) {
        $this->blockThresholds = [];
        $this->setThresholds($this->blockThresholds, $fiveMin, $hour, $day);
    }

    public function setForgivenThreshold($numForgiven) {
        if($numForgiven <= 0) {
            throw new \InvalidArgumentException("numForgiven cannot be <= 0");
        }
        $this->forgivenThreshold = $numForgiven;
    }

    /*
    *  Set the socket read timeout in milliseconds.
    *  @param $milliseconds must be < 1000 and > 0
    */
    public function setReadTimeout($milliseconds) {
        if($milliseconds >= 1000 || $milliseconds <= 0) {
            throw new \InvalidArgumentException("milliseconds must be < 1000 and > 0");
        }

        $this->readTimeout = array("sec" => 0, "usec" => $milliseconds * 1000);
    }

    /*
    * Add the $impactAmount to the fiveMin, hour, and day windows for $ip
    * @param $ip long|string
    * @param $impactAmount int
    * @return one of PASS, CAPTCHA, or BLOCK based on the updated data.
    *         always returns PASS if the $ip is whitelisted, else
    *         returns BLOCK if the $ip is blacklisted.
    */
    public function logIP($ip, $impactAmount) {
        $this->ensureThresholdsSet();

        // convert string like '127.0.0.1' to long
        if(is_string($ip)) { $ip = ip2long($ip); }

        if($impactAmount <= 0) {
            throw new \InvalidArgumentException("logIP impactAmount cannot be <= 0");
        }

        $cmd = 0x01; // kawana command byte for LogIP
        $bytes = pack("CVV", $cmd, $ip, $impactAmount);
        $resp = $this->sendAndRecv($bytes);
        return $this->checkIPData($resp);
    }

    /*
    * Subtract a percentage of the captcha threshold from the $ip's 
    * fiveMin, hour, and day windows.
    * @param $percent optional between 0.0 and 1.0, defaults to 0.5
    * @return one of PASS, CAPTCHA, or BLOCK based on the updated data.
    *         always returns PASS if the $ip is whitelisted, else
    *         returns BLOCK if the $ip is blacklisted.
    */
    public function forgiveIP($ip, $percent = 0.5) {
        $this->ensureThresholdsSet();

        // convert string like '127.0.0.1' to long
        if(is_string($ip)) { $ip = ip2long($ip); }

        if($percent <= 0.0 || $percent > 1.0) {
            throw new \InvalidArgumentException("forgiveIP percent cannot be <= 0.0 or > 1.0");
        }

        $fiveMin = $this->captchaThresholds['fiveMin'];
        $hour = $this->captchaThresholds['hour'];
        $day = $this->captchaThresholds['day'];

        $cmd = 0x02; // kawana command byte for ForgiveIP
        $bytes = pack("CVVVV", $cmd, $ip, $fiveMin, $hour, $day);
        $resp = $this->sendAndRecv($bytes);
        return $this->checkIPData($resp);
    }

    public function whitelistIP($ip) {
        $this->setBlackWhite($ip, 1);
    }

    public function unWhitelistIP($ip) {
        $this->setBlackWhite($ip, 2);
    }

    public function blacklistIP($ip) {
        $this->setBlackWhite($ip, 3);
    }

    public function unBlacklistIP($ip) {
        $this->setBlackWhite($ip, 4);
    }

    protected function setBlackWhite($ip, $modifier) {
        $validMods = [1, 2, 3, 4];
        if(!in_array($modifier, $validMods)) {
            throw new \InvalidArgumentException("Invalid BlackWhite modifier: $modifier");
        }

        // convert string like '127.0.0.1' to long
        if(is_string($ip)) { $ip = ip2long($ip); }

        $cmd = 0x03; // kawana command byte for BlackWhite
        $bytes = pack("CVC", $cmd, $ip, $modifier);
        $this->sendAndRecv($bytes);
    }

    protected function checkIPData($resp) {
        $result = unpack("V3impacts/Sforgiven/Cbw", $resp);

        foreach(['impacts1', 'impacts2', 'impacts3', 'forgiven', 'bw'] as $field) {
            if(!isset($result[$field])) {
                throw new \Exception("Invalid response from server");
            }
        }

        $maxImpacts = [
            'fiveMin' => $result['impacts1'],
            'hour'    => $result['impacts2'],
            'day'     => $result['impacts3']
        ];

        // check blacklist and whitelist
        if($result['bw'] & 0x01) {
            return self::PASS;
        }
        if($result['bw'] & 0x02) {
            return self::BLOCK;
        }

        // check block
        foreach($maxImpacts as $time => $impact) {
            if($impact >= $this->blockThresholds[$time]) {
                return self::BLOCK;
            }
        }

        // check captcha
        foreach($maxImpacts as $time => $impact) {
            if($impact >= $this->captchaThresholds[$time]) {
                return self::CAPTCHA;
            }
        }

        // check forgiven
        if($result['forgiven'] >= $this->forgivenThreshold) {
            return self::BLOCK;
        }

        return self::PASS;
    }

    /*
     * @return string response bytes
     */
    protected function sendAndRecv($bytes) {
        $responseLength = 15; // kawana response is 15 bytes

        // create socket
        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if($socket === false) {
            throwSocketException('socket_create', $socket);
        }

        if(null !== $this->readTimeout && !socket_set_option($socket,SOL_SOCKET, SO_RCVTIMEO, $this->readTimeout)) {
            throwSocketException('socket_set_options', $socket);
        }

        // connect to kawana server
        $connected = @socket_connect($socket, $this->address, $this->port);
        if ($connected === false) {
            throwSocketException('socket_connect', $socket);
        } 

        // write bytes to server
        $numBytes = strlen($bytes);
        for($sent = 0; $sent < $numBytes;) {
            $n = socket_write($socket, $bytes);
{'fraction_non_alphanumeric': 0.1393897364771151, 'fraction_numerical': 0.015873015873015872, 'mean_word_length': 2.2229947851999006, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 97, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 6, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7609305', 'n_tokens_mistral': 4181, 'n_tokens_neox': 3822, 'n_words': 961}