| <?php
declare(strict_types=1);
/*
 * sync*gw SpamBot Bundle
 *
 * @copyright  https://syncgw.com, 2013 - 2022
 * @author     Florian Daeumling, https://syncgw.com
 * @license    http://opensource.org/licenses/lgpl-3.0.html
 */
namespace syncgw\SpamBotBundle\Module;
use Contao\Environment;
use Contao\System;
class SpamBot extends System {
    // modes
    const MOD_FIRST = 1;
    const MOD_SPAM  = 2;
    const MOD_HAM   = 3;
    // module type
    const TYP_IP    = 1;
    const TYP_MAIL  = 2;
    // spam types
    const NOTFOUND  = 0x01;
    const SPAM      = 0x02;
    const HAM       = 0x04;
    const WHITEL    = 0x08;
    const BLACKL    = 0x10;
    const LOADED    = 0x20;
    // text translation
    public static $Status = [
        self::NOTFOUND  => 'NotFound',
        self::SPAM      => 'Spam',
        self::HAM       => 'Ham',
        self::WHITEL    => 'WhiteList',
        self::BLACKL    => 'BlackList',
        self::LOADED    => 'Loaded',
    ];
    /*
     * module ID
     * @var int
     */
    public $modID;
    // extended information
    public $ExtInfo;
    // error message
    public $ErrMsg;
    // HTTP header received
    public $Header = [];
    // database pointer
    public $Db;
    /*
     * internal cache
     * @var array
     */
    protected $arrData = [];
    // use IP as is
    protected $Raw = FALSE;
    // HTTP buffer
    protected $Buffer = [];
    /**
     * Initialize class
     *
     * @param module id
     */
    public function __construct(int $modID = 0) {
        parent::__construct();
        $this->Db = \Contao\Database::getInstance();
        $this->modID = $modID;
    }
    /**
     * Load variables
     */
    public function __get($strKey) {
        if (!isset($this->arrData[$strKey])) {
            // record data already loaded?
            if (NULL === $this->Fields)
                return NULL;
            $rc = $this->Db->prepare('SELECT '.implode(',', array_keys($this->Fields)).' from tl_module WHERE id=?')->execute($this->modID);
            foreach ($this->Fields as $k => $v) {
                if ($v) {
                    $this->arrData[$k] = deserialize($rc->$k);
                    if (!is_array($this->arrData[$k]))
                        $this->arrData[$k] = [];
                } else
                    $this->arrData[$k] = $rc->$k;
            }
            $this->Fields = NULL;
        }
        return $this->arrData[$strKey];
    }
    /**
     * Save variable
     */
    public function __set($strKey, $varValue)  {
        $this->arrData[$strKey] = $varValue;
    }
    /**
     * Perform parallel checking
     *
     * @param function to call
     * @param IP address
     * @param mail address
     * @param call mod
     * @param 1=Execution time; 2=Additional info
     *
     * @return array (SpamBot::Status, status message, execution time)
     */
    public function callMods(int $func, string $ip, string $mail, int $mod = self::MOD_SPAM, int $info = 0): array {
        // get configured engines
        $conf = $this->Db->prepare('SELECT spambot_engines FROM tl_module WHERE id=?')->execute($this->modID);
        if (!is_array($conf = deserialize($conf->spambot_engines)))
            return ['Intern' => [self::NOTFOUND, 'No providwr selected', 0]];
        // start time
        $start = [];
        // handler ID
        $hd = [];
        // end time
        $end = [];
        // return array: name => array (SpamBot::Status, status message, execution time)
        $rc = [];
        // number of available active handlers
        $active = 0;
        // call all engines in parallel
        foreach ($conf as $name) {
            if ($info)
                $start[$name] = microtime(TRUE);
            $rc[$name] = NULL;
            // var_dump(Environment::get('httpHost').'/bundles/spambot/SpamBotCall.php'.'?Mod='.$this->modID.'&Class='.$name.'&Func='.$func.
            //        '&IP='.base64_encode($ip).'&Mail='.base64_encode($mail).'&ExtInfo='.(2 === $info ? '1' : '0'));
            if (!($hd[$name] = self::openHTTP(Environment::get('httpHost'),
                                              '/bundles/spambot/SpamBotCall.php'.
                                              '?Mod='.$this->modID.'&Class='.$name.'&Func='.$func.
                                              '&IP='.base64_encode($ip).'&Mail='.base64_encode($mail).'&ExtInfo='.(2 === $info ? '1' : '0')))) {
                $rc[$name] = [self::NOTFOUND, $name.': '.$this->ErrMsg];
            } else
                $active++;
        }
        // wait until all handler has terminated
        while ($active > 0) {
            foreach ($conf as $name) {
                // done?
                if (!$hd[$name])
                    continue;
                // get response
                $r = self::readHTTP($hd[$name]);
                if ($info)
                    $end[$name] = microtime(TRUE);
                // validate return value
                if ($r === FALSE) {
                    // simualte not found on error
                    $rc[$name] = [self::NOTFOUND, $name.': '.$this->ErrMsg, 0];
                    fclose($hd[$name]);
                    $hd[$name] = NULL;
                    $r = NULL;
                }
                // any valid response
                if (is_null($r) || !strlen($r))
                    continue;
                // check wait mode
                switch ($mod) {
                case self::MOD_FIRST:
                    $active = 1;
                    // no break
                case self::MOD_SPAM:
                case self::MOD_HAM:
                default:
                    $active--;
                    break;
                }
                // build return value
                if (FALSE === ($a = unserialize($r)))
                    $rc[$name] = [self::NOTFOUND, $name.': '.$r, 0];
                else {
                    $rc[$name] = $a;
                    // set no execution time
                    $rc[$name][2] = 0;
                }
            }
        }
        // cleanup
        foreach ($conf as $name) {
            if ($hd[$name])
                fclose($hd[$name]);
            if ($info)
                $rc[$name][2] = round(($end[$name] - $start[$name]) * 1000, 3);
        }
        return $rc;
    }
    /**
     * Default check data
     *
     * @param typ to check
     * @param IP address
     * @param mail address
     *
     * @return array (SpamBot::Status, status message) or received status codes (raw=TRUE)
     */
    public function check(int $typ, string $ip, string $mail): array {
        if (self::TYP_MAIL === $typ)
            return [self::NOTFOUND, sprintf($GLOBALS['TL_LANG']['SpamBot']['SpamBot']['notfound'], $this->Name)];
        // build query
        $chk = $this->Raw ? $ip : implode('.', array_reverse(explode('.', $ip))).$GLOBALS['SpamBot']['Engines'][$this->Name]['DNSBL'];
        $rc = [];
        $ext = FALSE;
        if (!$this->Raw)
            $this->ExtInfo = '<fieldset style="padding:3px"><div style="color:blue;">'.
                             'Checking <strong>'.(self::TYP_IP === $typ ? $ip : $mail).'</strong> <br />';
        if (version_compare(PHP_VERSION, '5.3', '>=')) {
            if ($r = @dns_get_record($chk, DNS_A + DNS_TXT)) {
                foreach ($r as $s) {
                    if ('A' === $s['type']) {
                        $rc[] = $s['ip'];
                        $this->ExtInfo .= 'Status received is <strong>'.$s['ip'].'</strong><br />';
                    } else {
                        $ext = TRUE;
                        if (isset($s['txt']))
                            $this->ExtInfo .= 'TXT record is <strong>'.$s['txt'].'</strong><br />';
                    }
                }
            }
            // hack for spamcop.net which does not return "A" record
            if ($ext && !count($rc))
                $rc[] = '127.0.0.2';
        } else {
            $rc[0] = gethostbyname($chk);
            $this->ExtInfo .= '<strong>Return code:</strong> '.$rc[0].'<br />';
        }
        $this->ExtInfo .= '</div></fieldset>';
        if (!count($rc))
            return [self::NOTFOUND, sprintf($GLOBALS['TL_LANG']['SpamBot']['generic']['notfound'], $this->Name)];
        if ('127' !== substr($rc[0], 0, 3))
            return [self::NOTFOUND, sprintf($GLOBALS['TL_LANG']['SpamBot']['generic']['err'], $this->Name, $rc[0])];
        // raw call?
        if ($this->Raw)
            return serialize($rc);
        // spam list
        $codes = is_array($GLOBALS['SpamBot']['Engines'][$this->Name]['Codes']) ?
                          $GLOBALS['SpamBot']['Engines'][$this->Name]['Codes'] : [];
        // module configuration array
        if (!is_array($conf = $this->{'spambot_'.strtolower($this->Name).'_mods'}))
            $conf = $GLOBALS['SpamBot']['Engines'][$this->Name]['Spam'];
        // check for Spam
        foreach ($rc as $stat) {
            foreach ($codes as $k => $v) {
                if (!in_array($k, $conf, TRUE))
                    continue;
                // wild card?
                if ($p = strpos($k, '*')) {
                    if (substr($k, 0, $p) === substr($stat, 0, $p))
                        return [self::SPAM, $this->Name.': '.$v];
                }
                if ($k === $stat)
                    return [self::SPAM, $this->Name.': '.$v];
            }
        }
        // check for Ham
        foreach ($rc as $stat) {
            foreach ($codes as $k => $v) {
                if (in_array($k, $conf, TRUE))
                    continue;
                // wild card?
                if ($p = strpos($k, '*')) {
                    if (substr($k, 0, $p) === substr($stat, 0, $p))
                    	return [self::HAM, $this->Name.': '.$v];
                }
                if ($k === $stat)
                    return [self::HAM, $this->Name.': '.$v];
            }
        }
        // not found
        return [self::NOTFOUND, sprintf($GLOBALS['TL_LANG']['SpamBot']['generic']['unsup'], $this->Name, $rc)];
    }
    /**
     * Open connection to host
     *
     * @param host name
     * @param URI
     * @param timeout
     *
     * @return mixed $pointer
     */
    public function openHTTP(string $host, string $uri, int $timeout = 10) {
        // prepare request (no fopen() usage because "allow_url_fopen=FALSE" may be set in PHP.INI)
        $req = 'GET '.$uri." HTTP/1.0\r\nHost: ".$host."\r\n\r\n";
        $sock = 80 != $_SERVER['SERVER_PORT'] ? 'ssl://' : NULL;
        $err = NULL;
        if (($fp = fsockopen($sock.$host, intval($_SERVER['SERVER_PORT']), $err, $this->ErrMsg, 10))) {
            fwrite($fp, $req);
            stream_set_timeout($fp, $timeout);
            $this->Header[(int) ($fp)] = [];
            $this->Buffer[(int) ($fp)] = NULL;
            if (($this->Buffer[(int) ($fp)] = self::readHTTP($fp)) === FALSE) {
                fclose($fp);
                return FALSE;
            }
        } else
            $this->ErrMsg = '['.$err.'] '.$this->ErrMsg;
        return $fp;
    }
    /**
     * Read data
     *
     * @param file pointer
     *
     * @return FALSE on error; else string
     */
    public function readHTTP($fp) {
        if (!is_resource($fp) || !$fp) {
            $this->ErrMsg = 'Connection to "'.$this->Name.'" is not a resource';
            return FALSE;
        }
        if ($this->Buffer[(int) ($fp)]) {
            $wrk = $this->Buffer[(int) ($fp)];
            $this->Buffer[(int) ($fp)] = NULL;
            return $wrk;
        }
        if (FALSE === ($wrk = fread($fp, 8192))) {
            $info = stream_get_meta_data($fp);
            if ($info['timed_out'])
                $this->ErrMsg = 'Connection to "'.$this->Name.'" timed out ('.$info['timed_out'].' sec.)';
            else
                $this->ErrMsg = 'Unspecific connection error to "'.$this->Name.'"';
            return FALSE;
        }
        if (is_array($this->Header[(int) ($fp)]))
            $do = !count($this->Header[(int) ($fp)]) ? 1 : 0;
        else
            $do = 0;
        if ($do && $wrk) {
            $this->Header[(int) ($fp)] = explode("\r\n", substr($wrk, 0, $pos = strpos($wrk, "\r\n\r\n")));
            $wrk = substr($wrk, $pos + 4);
            if (!$wrk)
                $wrk = NULL;
            // we use this approach to get "HTTP/1.0 200 OK" as well as "HTTP/1.1 200 OK"
            if (count($this->Header[(int) ($fp)]) && FALSE === strpos($this->Header[(int) ($fp)][0], '200 OK')) {
                $this->ErrMsg = $this->Header[(int) ($fp)][0];
                return FALSE;
            }
        }
        return $wrk;
    }
}
?>
 |