<?php
/**
 * @class netNntp
 *
 * @package Clearbricks
 * @subpackage Network
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */

class netNntp extends netSocket
{
    const SERVER_READY         = 200;
    const SERVER_READY_NO_POST = 201;
    const GROUP_SELECTED       = 211;
    const INFORMATION_FOLLOWS  = 215;
    const ARTICLE_HEAD_BODY    = 220;
    const ARTICLE_HEAD         = 221;
    const ARTICLE_BODY         = 222;
    const ARTICLE_OVERVIEW     = 224;
    const NEW_ARTICLES         = 230;
    const ARTICLE_POST_OK      = 240;
    const ARTICLE_POST_READY   = 340;
    const AUTH_ACCEPT          = 281;
    const MORE_AUTH_INFO       = 381;
    const AUTH_REQUIRED        = 480;
    const AUTH_REJECTED        = 482;
    const NOT_IMPLEMENTED      = 500;
    const NO_PERMISSION        = 502;

    protected $host;
    protected $port;
    protected $user;
    protected $password;

    protected $proxy_host;
    protected $proxy_port;
    protected $proxy_user;
    protected $proxy_pass;
    protected $use_proxy;

    public function __construct($host, $port = 119, $user = null, $password = null, $timeout = 10)
    {
        $this->host     = $host;
        $this->port     = (integer) $port;
        $this->user     = $user;
        $this->password = $password;
        $this->_timeout = $timeout;
    }

    public function write($data)
    {
        if (!is_array($data)) {
            $data = $data . "\r\n";
        }
        return parent::write($data);
    }

    public function close()
    {
        if ($this->isOpen()) {
            $this->sendRequest('quit');
            parent::close();
        }
    }

    public function open()
    {
        if ($this->isOpen()) {
            return true;
        }

        if ($this->use_proxy) {
            $this->_host = $this->proxy_host;
            $this->_post = $this->proxy_port;
        } else {
            $this->_host = $this->host;
            $this->_port = $this->port;
        }

        $rsp = parent::open();

        if ($this->isOpen()) {
            if ($this->use_proxy) {
                $data[] = 'CONNECT ' . $this->host . ':' . $this->port . ' HTTP/1.0';
                if ($this->proxy_user && $this->proxy_pass) {
                    $data[] =
                    'Proxy-Authorization: Basic ' .
                    base64_encode($this->proxy_user . ':' . $this->proxy_pass);
                }

                foreach ($this->write($data) as $i => $v) {
                    if ($i == 0) {
                        if (stristr($v, '200 Connection established')) {
                            continue;
                        } else {
                            $rsp = [
                                'status'  => self::NO_PERMISSION, # Assign it to something dummy
                                'message' => "No permission"
                            ];
                            break;
                        }
                    }
                    if ($i == 2) {
                        $rsp = $this->parseResponse($v);
                        break;
                    }
                }
            } else {
                $rsp = $this->parseResponse($rsp->current());
            }

            if (($rsp['status'] == self::SERVER_READY) || ($rsp['status'] == self::SERVER_READY_NO_POST)) {
                $this->sendRequest("mode reader");
                if ($this->user) {
                    $rsp = $this->parseResponse($this->sendRequest('authinfo user ' . $this->user));

                    if ($rsp['status'] == self::MORE_AUTH_INFO) {
                        $rsp = $this->parseResponse($this->sendRequest('authinfo pass ' . $this->password));

                        if ($rsp['status'] == self::AUTH_ACCEPT) {
                            return true;
                        }
                    }
                } else {
                    return true;
                }
            }

            throw new Exception($rsp['status'] . ' - ' . $rsp['message']);
        }
    }

    public function setUser($user, $password = null)
    {
        $this->user     = $user;
        $this->password = $password;

        if ($this->isOpen()) {
            $this->close();
            $this->open();
        }
    }

    public function setProxy($proxy_host, $proxy_port = null, $proxy_user = null, $proxy_pass = null)
    {
        $this->proxy_host = $proxy_host;
        $this->proxy_port = $proxy_port;
        $this->proxy_user = $proxy_user;
        $this->proxy_pass = $proxy_pass;

        if ((strcmp($this->proxy_host, "") != 0) && (strcmp($this->proxy_port, "") != 0)) {
            $this->use_proxy = true;
        } else {
            $this->use_proxy = false;
        }
    }

    public function getGroupsList($group_pattern = null)
    {
        $rsp = $this->write('list active ' . $group_pattern);
        $r   = $this->parseResponse($rsp->current());

        if ($r['status'] == self::INFORMATION_FOLLOWS) {
            # List groups
            $result = [];
            foreach ($rsp as $buf) {
                if (preg_match('/^\.\s*$/', $buf)) {
                    break;
                }

                list($group, $last, $first, $post) = preg_split('/\s+/', $buf, 4);
                $result[$group]                    = [
                    'desc'  => '',
                    'last'  => trim($last),
                    'first' => trim($first),
                    'post'  => strtolower(trim($post))
                ];
            }

            # Get groups descriptions
            $rsp = $this->write(['list newsgroups ' . $group_pattern]);
            $r   = $this->parseResponse($rsp->current());

            if ($r['status'] == self::INFORMATION_FOLLOWS) {
                foreach ($rsp as $buf) {
                    if (self::eot($buf)) {
                        break;
                    }

                    list($group, $desc) = preg_split('/\s+/', $buf, 2);
                    if (isset($result[$group])) {
                        $result[$group]['desc'] = text::toUTF8(trim($desc));
                    }
                }
            }

            return $result;
        }

        throw new Exception($r['message'] . ' - (' . $r['status'] . ')');
    }

    public function joinGroup($group)
    {
        $rsp = $this->parseResponse($this->sendRequest('group ' . $group));

        if ($rsp['status'] == self::GROUP_SELECTED) {
            $result = preg_split("/\s/", $rsp['message']);

            return [
                'count'    => $result[0],
                'start_id' => $result[1],
                'end_id'   => $result[2],
                'group'    => $result[3]
            ];
        }

        throw new Exception($rsp['message'] . ' - (' . $rsp['status'] . ')');
    }

    public function getArticleList($group = null)
    {
        $rsp = $this->write('listgroup ' . $group);
        $r   = $this->parseResponse($rsp->current());

        if ($r['status'] == self::GROUP_SELECTED) {
            $res = [];
            foreach ($rsp as $i => $buf) {
                if (self::eot($buf)) {
                    break;
                }
                $res[] = trim($buf);
            }
            return $res;
        }

        throw new Exception($r['message'] . ' - (' . $r['status'] . ')');
    }

    public function getNewArticles($ts, $group)
    {
        $ts = $ts + dt::getTimeOffset('UTC', $ts);

        # First try with newnews
        $rsp = $this->write('newnews ' . $group . ' ' . dt::str('%y%m%d %H%M%S', $ts) . ' GMT');
        $r   = $this->parseResponse($rsp->current());

        if ($r['status'] == self::NEW_ARTICLES) {
            $res = [];
            $rsp->current(); # we don't want first matched article
            foreach ($rsp as $buf) {
                if (self::eot($buf)) {
                    break;
                }
                $res[] = trim($buf);
            }
            return $res;
        } else {
            # newnews is not implemented, use xhdr instead
            # First, we need to join the group
            $g = $this->joinGroup($group);

            if ($g['count'] > 1000) {
                $g['start_id'] = $g['end_id'] - 1000;
            }

            # Then, xhdr on all group messages
            $rsp = $this->write('xhdr date ' . $g['start_id'] . '-');
            $r   = $this->parseResponse($rsp->current());

            if ($r['status'] == self::ARTICLE_HEAD) {
                $ts = $ts + dt::getTimeOffset('UTC', $ts);

                $res = [];
                foreach ($rsp as $buf) {
                    if (self::eot($buf)) {
                        break;
                    }
                    $buf = preg_split('/\s/', $buf, 2);
                    if (strtotime($buf[1]) > $ts) {
                        $res[] = $buf[0];
                    }
                }
                return $res;
            }
        }

        throw new Exception($r['message'] . ' - (' . $r['status'] . ')');
    }

    public function getHeader($message_id, &$header = '')
    {
        $rsp = $this->write('head ' . $message_id);
        $r   = $this->parseResponse($rsp->current());

        if ($r['status'] == self::ARTICLE_HEAD || $r['status'] == self::ARTICLE_HEAD_BODY) {
            $header = '';
            foreach ($rsp as $buf) {
                if (self::eot($buf)) {
                    break;
                }
                $header .= $buf;
            }

            return new nntpMessage($header);
        }

        throw new Exception($r['message'] . ' - (' . $r['status'] . ')');
    }

    public function getArticle($message_id, &$article = '')
    {
        $rsp = $this->write('article ' . $message_id);
        $r   = $this->parseResponse($rsp->current());

        if ($r['status'] == self::ARTICLE_BODY || $r['status'] == self::ARTICLE_HEAD_BODY) {
            $article = '';
            foreach ($rsp as $buf) {
                if (self::eot($buf)) {
                    break;
                }
                $article .= $buf;
            }
            return new nntpMessage($article);
        }

        throw new Exception($r['message'] . ' - (' . $r['status'] . ')');
    }

    public function postArticle($headers = [], $content)
    {
        if (!is_array($headers)) {
            return false;
        }

        if (empty($headers['From'])) {
            throw new Exception('No "From" header in message');
        }

        $headers['Mime-Version']              = '1.0';
        $headers['Content-Type']              = 'text/plain; charset=UTF-8';
        $headers['Content-Transfer-Encoding'] = 'quoted-printable';

        $content = mailConvert::rewrap($content);
        $content = preg_replace('/^\./msu', '..$1', $content);
        $content = text::QPEncode($content);

        $data = [];
        # Headers
        foreach ($headers as $k => $v) {
            $data[] = $k . ': ' . $v;
        }

        # Blank line
        $data[] = '';

        # Body
        foreach (preg_split("/\r?\n/msu", $content) as $l) {
            $data[] = $l;
        }

        # EOT
        $data[] = '.';

        $this->sendRequest('post');

        $rsp = $this->write($data);
        $r   = $this->parseResponse($rsp->current());
        if ($r['status'] == self::ARTICLE_POST_OK) {
            return true;
        }

        throw new Exception($r['message'] . ' - (' . $r['status'] . ')');
    }

    protected static function eot($l)
    {
        return preg_match('/^\.\s*$/', $l);
    }

    protected function assertOpen()
    {
        if (!$this->isOpen()) {
            throw new Exception('NNTP connexion not available');
        }
    }

    protected function parseResponse($rsp)
    {
        return [
            'status'  => substr($rsp, 0, 3),
            'message' => rtrim(substr($rsp, 4), "\r\n")
        ];
    }

    protected function sendRequest($request)
    {
        $this->assertOpen();

        $rsp = $this->write($request);
        $this->flush();
        return $rsp->current();
    }
}
