<?php

namespace NukeViet\Core;

/**
 * Request
 *
 * @package
 * @author
 * @copyright VINADES.,JSC
 * @version 2010
 * @access public
 */
class Request
{
    const IS_HEADERS_SENT = 'Warning: Headers already sent';

    const INCORRECT_IP = 'Incorrect IP address specified';

    const INCORRECT_ORIGIN = 'Incorrect Origin specified';

    const REQUEST_BLOCKED = 'Your request is blocked';

    public $session_id;

    public $doc_root;

    public $site_url;

    public $base_siteurl;

    public $base_adminurl;

    public $server_name;

    public $server_protocol;

    public $server_port;

    public $my_current_domain;

    public $headerstatus;

    public $session_save_path;

    public $cookie_path;

    public $cookie_domain;

    public $referer;

    private $origin;

    private $method;

    /**
     * @var integer
     *
     * 0 cross origin referer
     * 1 same origin referer
     * 2 không có referer
     */
    public $referer_key;

    /**
     * @var integer
     *
     * 0 cross origin
     * 1 same origin
     * 2 no origin header
     */
    private $origin_key;

    public $referer_host = '';

    public $referer_queries = false;

    public $request_uri;

    public $user_agent;

    public $search_engine = '';

    private $request_default_mode = 'request';

    private $allow_request_mods = [
        'get',
        'post',
        'request',
        'cookie',
        'session',
        'env',
        'server'
    ];

    private $cookie_prefix = 'NV4';

    private $session_prefix = 'NV4';

    private $cookie_key = 'nv4';

    private $secure = false;

    private $httponly = true;

    private $ip_addr;

    private $remote_ip;

    private $str_referer_blocker = false;

    private $engine_allowed = [];

    // Cac tags bi cam dung mac dinh, co the go bo bang cach thay doi cac tags cho phep cua NV_ALLOWED_HTML_TAGS
    private $disabletags = [
        'applet',
        'body',
        'basefont',
        'head',
        'html',
        'id',
        'meta',
        'xml',
        'blink',
        'link',
        'style',
        'script',
        'iframe',
        'frame',
        'frameset',
        'ilayer',
        'layer',
        'bgsound',
        'title',
        'base'
    ];

    protected $remoteAttrCheck = [
        'action' => ['form'],
        'src' => ['iframe', 'embed'],
        'data' => ['object']
    ];

    /**
     * Các attr bị cấm, sẽ bị lọc bỏ.
     * - Tất cả các arrt bắt đầu bằng on
     * - Các attr bên dưới
     */
    private $disabledattributes = [
        'action',
        'background',
        'codebase',
        'dynsrc',
        'lowsrc',
        'allownetworking', // Control a SWF file’s access to network functionality by setting the allowNetworking parameter = internal
        'allowscriptaccess', // Loại bỏ điều khiển cho phép javascript trong embed, tự động đặt = never
        'fscommand', // attacker can use this when executed from within an embedded Flash object
        'seeksegmenttime' // this is a method that locates the specified point on the element’s segment time line and begins playing from that point. The segment consists of one repetition of the time line including reverse play using the AUTOREVERSE attribute.
    ];

    private $disablecomannds = [
        'base64_decode',
        'cmd',
        'passthru',
        'eval',
        'exec',
        'system',
        'fopen',
        'fsockopen',
        'file',
        'file_get_contents',
        'readfile',
        'unlink'
    ];

    /**
     * @var array
     */
    protected $corsHeaders = [
        'Access-Control-Allow-Origin' => '*',
        'Access-Control-Allow-Headers' => 'Origin, X-Requested-With, Content-Type', // Các Header được phép trong CORS
        'Access-Control-Allow-Methods' => 'PUT, GET, POST, DELETE, OPTIONS', // Các phương thước được phép trong CORS
        'Access-Control-Allow-Credentials' => 'true', // Cho phép gửi cookie trong truy vấn CORS
        'Access-Control-Max-Age' => 10 * 60 * 60, // 10 min, max age for Chrome. Thời gian cache preflight request (request OPTIONS kiểm tra)
        'Vary' => 'Origin' // Thông báo cho trình duyệt biết, mỗi Origin khác nhau sẽ có mỗi phản hồi khác nhau thay vì dùng *
    ];

    /**
     * @since 4.4.01
     */
    protected $restrictCrossDomain = true;
    protected $validCrossDomains = [];
    protected $validCrossIPs = [];

    protected $isOriginValid = false;
    protected $isRefererValid = false;

    protected $isIpValid = false;

    protected $isRestrictDomain = true;
    protected $validDomains = [];

    /**
     * @param array $config
     * @param string $ip Client IP
     * @param \NukeViet\Core\Server|boolean $nv_Server
     */
    public function __construct($config, $ip, $nv_Server = false)
    {
        if (isset($config['allowed_html_tags']) and is_array($config['allowed_html_tags'])) {
            $this->disabletags = array_diff($this->disabletags, $config['allowed_html_tags']);
        }
        if (!empty($config['allow_request_mods'])) {
            if (!is_array($config['allow_request_mods'])) {
                $config['allow_request_mods'] = [$config['allow_request_mods']];
            }
            $this->allow_request_mods = array_intersect($this->allow_request_mods, $config['allow_request_mods']);
        }
        if (!empty($config['request_default_mode']) and in_array($config['request_default_mode'], $this->allow_request_mods)) {
            $this->request_default_mode = $config['request_default_mode'];
        }
        if (!empty($config['cookie_secure'])) {
            $this->secure = true;
        }
        if (!empty($config['cookie_httponly'])) {
            $this->httponly = true;
        }
        if (!empty($config['cookie_prefix'])) {
            $this->cookie_prefix = preg_replace('/[^a-zA-Z0-9\_]+/', '', $config['cookie_prefix']);
        }
        if (!empty($config['session_prefix'])) {
            $this->session_prefix = preg_replace('/[^a-zA-Z0-9\_]+/', '', $config['session_prefix']);
        }
        if (!empty($config['sitekey'])) {
            $this->cookie_key = $config['sitekey'];
        }
        if (!empty($config['str_referer_blocker'])) {
            $this->str_referer_blocker = true;
        }
        $this->engine_allowed = (array) $config['engine_allowed'];
        if (empty($ip)) {
            $ip = $_SERVER['REMOTE_ADDR'];
        }
        $this->remote_ip = $ip;

        if (defined('NV_ADMIN')) {
            $this->restrictCrossDomain = !empty($config['crossadmin_restrict']) ? true : false;
            $this->validCrossDomains = !empty($config['crossadmin_valid_domains']) ? ((array) $config['crossadmin_valid_domains']) : [];
            $this->validCrossIPs = !empty($config['crossadmin_valid_ips']) ? ((array) $config['crossadmin_valid_ips']) : [];
        } else {
            $this->restrictCrossDomain = !empty($config['crosssite_restrict']) ? true : false;
            $this->validCrossDomains = !empty($config['crosssite_valid_domains']) ? ((array) $config['crosssite_valid_domains']) : [];
            $this->validCrossIPs = !empty($config['crosssite_valid_ips']) ? ((array) $config['crosssite_valid_ips']) : [];
        }

        $this->isRestrictDomain = !empty($config['domains_restrict']) ? true : false;
        $this->validDomains = !empty($config['domains_whitelist']) ? ((array) $config['domains_whitelist']) : [];

        if (preg_match('#^(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$#', $ip)) {
            $ip2long = ip2long($ip);
        } else {
            if (substr_count($ip, '::')) {
                $ip = str_replace('::', str_repeat(':0000', 8 - substr_count($ip, ':')) . ':', $ip);
            }
            $ip = explode(':', $ip);
            $r_ip = '';
            foreach ($ip as $v) {
                $r_ip .= str_pad(base_convert($v, 16, 2), 16, 0, STR_PAD_LEFT);
            }
            $ip2long = base_convert($r_ip, 2, 10);
        }

        if ($ip2long == - 1 or $ip2long === false) {
            trigger_error(Request::INCORRECT_IP, 256);
        }
        $this->ip_addr = $ip2long;

        $this->cookie_key = md5($this->cookie_key);

        if ($nv_Server === false) {
            $nv_Server = new Server();
        }
        $this->Initialize($nv_Server);
        $this->get_cookie_save_path();

        $_ssl_https = (isset($config['ssl_https'])) ? $config['ssl_https'] : 0;
        $this->sessionStart($_ssl_https);
        $_REQUEST = array_merge($_POST, array_diff_key($_GET, $_POST));
    }

    /**
     * Request::get_Env()
     *
     * @param mixed $key
     * @return
     */
    private function get_Env($key)
    {
        if (!is_array($key)) {
            $key = [$key];
        }
        foreach ($key as $k) {
            if (isset($_SERVER[$k])) {
                return $_SERVER[$k];
            } elseif (isset($_ENV[$k])) {
                return $_ENV[$k];
            } elseif (@getenv($k)) {
                return @getenv($k);
            } elseif (function_exists('apache_getenv') and apache_getenv($k, true)) {
                return apache_getenv($k, true);
            }
        }
        return '';
    }

    /**
     * @param \NukeViet\Core\Server $nv_Server
     */
    private function Initialize($nv_Server)
    {
        if (sizeof($_GET)) {
            $array_keys = array_keys($_GET);
            foreach ($array_keys as $k) {
                if (!preg_match('/^[a-zA-Z0-9\_]+$/', $k) or is_numeric($k)) {
                    unset($_GET[$k]);
                }
            }
        }
        if (sizeof($_POST)) {
            $array_keys = array_keys($_POST);
            foreach ($array_keys as $k) {
                if ((!preg_match('/^[a-zA-Z0-9\_]+$/', $k) and $k != 'g-recaptcha-response') or is_numeric($k)) {
                    unset($_POST[$k]);
                }
            }
        }
        if (sizeof($_COOKIE)) {
            $array_keys = array_keys($_COOKIE);
            foreach ($array_keys as $k) {
                if (!preg_match('/^[a-zA-Z0-9\_]+$/', $k) or is_numeric($k)) {
                    @setcookie($k, '', NV_CURRENTTIME - 3600);
                    unset($_COOKIE[$k]);
                }
            }
        }
        if (sizeof($_FILES) and strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
            $array_keys = array_keys($_FILES);
            foreach ($array_keys as $k) {
                if (!preg_match('/^[a-zA-Z0-9\_]+$/', $k) or is_numeric($k)) {
                    unset($_FILES[$k]);
                }
            }
        }
        $query = http_build_query($_GET);
        $_SERVER['QUERY_STRING'] = $query;
        $_SERVER['argv'] = [$query];
        $this->request_uri = (empty($_SERVER['REQUEST_URI'])) ? $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING'] : $_SERVER['REQUEST_URI'];
        $doc_root = isset($_SERVER['DOCUMENT_ROOT']) ? preg_replace('/[\/]+$/', '', str_replace(DIRECTORY_SEPARATOR, '/', $_SERVER['DOCUMENT_ROOT'])) : '';

        if (defined('NV_BASE_SITEURL')) {
            $base_siteurl = preg_replace('/[\/]+$/', '', NV_BASE_SITEURL);
        } else {
            $base_siteurl = $nv_Server->getWebsitePath();
        }

        if (NV_ROOTDIR !== $doc_root . $base_siteurl) {
            $doc_root = NV_ROOTDIR;
            $count = substr_count($base_siteurl, '/');
            for ($i = 0; $i < $count; ++$i) {
                $doc_root = preg_replace('#\/[^\/]+$#', '', $doc_root);
            }
            $_SERVER['DOCUMENT_ROOT'] = $doc_root;
        }
        $_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . $_SERVER['PHP_SELF'];
        $_SERVER['SERVER_PORT'] = $this->get_Env('SERVER_PORT');
        $_SERVER['SERVER_PROTOCOL'] = $this->get_Env('SERVER_PROTOCOL');

        if (defined('NV_SERVER_NAME')) {
            $this->server_name = NV_SERVER_NAME;
        } else {
            $this->server_name = $nv_Server->getServerHost();
        }
        if (defined('NV_SERVER_PROTOCOL')) {
            $this->server_protocol = NV_SERVER_PROTOCOL;
        } else {
            $this->server_protocol = $nv_Server->getServerProtocol();
        }
        if (defined('NV_SERVER_PORT')) {
            $this->server_port = NV_SERVER_PORT;
        } else {
            $this->server_port = $nv_Server->getServerPort();
        }

        $this->base_siteurl = $base_siteurl;
        $this->base_adminurl = $base_siteurl . (NV_ADMINDIR != '' ? '/' . NV_ADMINDIR : '');
        $this->doc_root = $doc_root;

        if (defined('NV_MY_DOMAIN')) {
            $this->my_current_domain = NV_MY_DOMAIN;
        } else {
            $this->my_current_domain = $nv_Server->getOriginalDomain();
        }

        $this->headerstatus = (substr(php_sapi_name(), 0, 3) == 'cgi') ? 'Status:' : $_SERVER['SERVER_PROTOCOL'];
        $this->site_url = $this->my_current_domain . $this->base_siteurl;
        $this->standardizeReferer();
        $this->standardizeOrigin();
        $this->method = strtoupper($this->get_Env(['REQUEST_METHOD', 'Method']));

        // CORS handle
        if (!empty($this->origin)) {
            $this->corsHeaders['Access-Control-Allow-Origin'] = $this->getAllowOriginHeaderValue();
            $hasControlRequestHeader = $this->get_Env(['HTTP_ACCESS_CONTROL_REQUEST_HEADERS', 'Access-Control-Request-Headers']);

            foreach ($this->corsHeaders as $header => $value) {
                header($header . ': ' . $value);
            }

            // Kiểm tra preflight request
            if ($this->method == 'OPTIONS' and !empty($hasControlRequestHeader)) {
                exit(0);
            }
        }

        if ($this->str_referer_blocker and !empty($_SERVER['QUERY_STRING']) and $this->referer_key == 0 and empty($this->search_engine)) {
            header('Location: ' . $this->site_url);
            exit(0);
        }

        $user_agent = (string) $this->get_Env('HTTP_USER_AGENT');
        $user_agent = substr(htmlspecialchars($user_agent), 0, 255);
        if (!empty($user_agent)) {
            $user_agent = trim($user_agent);
        }
        if (empty($user_agent) or $user_agent == '-') {
            $user_agent = 'none';
        }
        $this->user_agent = $user_agent;
        $_SERVER['HTTP_USER_AGENT'] = $user_agent;

        // Cross-Site handle
        if (sizeof($_POST) or $this->method == 'POST') {
            if ($this->origin_key == 0 or $this->referer_key !== 1) {
                if (!$this->restrictCrossDomain or in_array($this->remote_ip, $this->validCrossIPs)) {
                    $this->isIpValid = true;
                }
            } else {
                $this->isIpValid = true;
            }
            if (!(($this->isRefererValid and (empty($this->origin) or $this->isOriginValid)) or $this->isIpValid)) {
                trigger_error(Request::REQUEST_BLOCKED, 256);
            }
        }
    }

    /**
     * Chuẩn hóa, kiểm tra Origin header
     */
    private function standardizeOrigin()
    {
        $this->origin = $this->get_Env(['HTTP_ORIGIN', 'Origin']);
        if (!empty($this->origin)) {
            $origin = parse_url($this->origin);
            if (isset($origin['scheme']) and in_array($origin['scheme'], ['http', 'https', 'ftp', 'gopher']) and isset($origin['host'])) {
                $_SERVER['HTTP_ORIGIN'] = ($origin['scheme'] . '://' . $origin['host'] . ((isset($origin['port']) and $origin['port'] != '80' and $origin['port'] != '443') ? (':' . $origin['port']) : ''));
                $this->origin = $_SERVER['HTTP_ORIGIN'];

                if ($this->my_current_domain == $this->origin) {
                    $this->origin_key = 1;
                } else {
                    $this->origin_key = 0;
                }
            } else {
                /*
                 * Origin có dạng `Origin: <scheme> "://" <hostname> [ ":" <port> ]`
                 * Nếu sai thì từ chối truy vấn
                 */
                trigger_error(Request::INCORRECT_ORIGIN, 256);
                unset($_SERVER['HTTP_ORIGIN']);
            }
        } else {
            $this->origin_key = 2;
        }
    }

    /**
     * Chuẩn hóa, kiểm tra Referer header
     */
    private function standardizeReferer()
    {
        $this->referer = $this->get_Env(['HTTP_REFERER', 'Referer']);
        if (!empty($this->referer)) {
            $ref = parse_url($this->referer);
            if (isset($ref['scheme']) and in_array($ref['scheme'], ['http', 'https', 'ftp', 'gopher']) and isset($ref['host'])) {
                $ref_origin = ($ref['scheme'] . '://' . $ref['host'] . ((isset($ref['port']) and $ref['port'] != '80' and $ref['port'] != '443') ? (':' . $ref['port']) : ''));
                // Server dạng IPv6 trực tiếp
                if (substr($ref['host'], 0, 1) == '[' and substr($ref['host'], -1) == ']') {
                    $ref['host'] = substr($ref['host'], 1, -1);
                }
                if (preg_match('/^' . preg_quote($ref['host'], '/') . '/', $this->server_name)) {
                    $this->referer_key = 1;
                } else {
                    $this->referer_key = 0;
                    if (!empty($this->engine_allowed)) {
                        foreach ($this->engine_allowed as $se => $v) {
                            if (preg_match('/' . preg_quote($v['host_pattern'], '/') . '/i', $ref['host'])) {
                                $this->search_engine = $se;
                                break;
                            }
                        }
                    }
                }
                $this->referer_host = $ref['host'];
                $tmp = [];
                $base = $this->referer;
                if (isset($ref['query']) and !empty($ref['query'])) {
                    list($base, $query_string) = explode('?', $this->referer);
                    parse_str($query_string, $parameters);
                    foreach ($parameters as $key => $value) {
                        if (preg_match('/^[a-zA-Z\_][a-zA-Z0-9\_]*$/', $key)) {
                            $tmp[$key] = $this->security_get($value, true);
                        }
                    }
                }
                if (!empty($tmp)) {
                    $this->referer_queries = $tmp;
                    $_SERVER['HTTP_REFERER'] = $base . '?' . http_build_query($tmp);
                } else {
                    $_SERVER['HTTP_REFERER'] = $base;
                }
                $this->referer = $_SERVER['HTTP_REFERER'];

                if (!$this->restrictCrossDomain or $this->referer_key === 1 or in_array($ref_origin, $this->validCrossDomains)) {
                    $this->isRefererValid = true;
                }
            } else {
                $this->referer_key = 0;
                $this->referer = '';
                unset($_SERVER['HTTP_REFERER']);
            }
        } else {
            $this->referer_key = 2;
            unset($_SERVER['HTTP_REFERER']);
        }
    }

    /**
     * Request::get_cookie_save_path()
     *
     * @return
     */
    private function get_cookie_save_path()
    {
        $this->cookie_path = $this->base_siteurl . '/';
        $cookie_domain = preg_replace('/^([w]{3})\./', '', $this->server_name);
        $this->cookie_domain = (preg_match('/^([0-9a-z][0-9a-z-]+\.)+[a-z]{2,6}$/', $cookie_domain)) ? '.' . $cookie_domain : '';
    }

    /**
     * Request::sessionStart()
     *
     * @return
     */
    private function sessionStart($_ssl_https)
    {
        if (headers_sent() or connection_status() != 0 or connection_aborted()) {
            trigger_error(Request::IS_HEADERS_SENT, 256);
        }

        $_secure = ($this->server_protocol == 'https' and $_ssl_https == 1) ? 1 : 0;
        session_set_cookie_params(NV_LIVE_SESSION_TIME, $this->cookie_path, $this->cookie_domain, $_secure, 1);

        session_name($this->cookie_prefix . '_sess');
        session_start();
        $session_id = session_id();

        $_SESSION = (isset($_SESSION) and is_array($_SESSION)) ? $_SESSION : [];
        if (sizeof($_SESSION)) {
            $array_keys = array_keys($_SESSION);
            foreach ($array_keys as $k) {
                if (!preg_match('/^[a-zA-Z0-9\_]+$/', $k) or is_numeric($k)) {
                    unset($_SESSION[$k]);
                }
            }
        }
        $this->session_id = $session_id;
    }

    /**
     * Request::chr_hexdec_callback()
     *
     * @param mixed $m
     * @return
     */
    private function chr_hexdec_callback($m)
    {
        return chr(hexdec($m[1]));
    }

    /**
     * Request::chr_callback()
     *
     * @param mixed $m
     * @return
     */
    private function chr_callback($m)
    {
        return chr($m[1]);
    }

    /**
     * Request::color_hex2rgb_callback()
     *
     * @param mixed $hex
     * @return
     */
    private function color_hex2rgb_callback($hex)
    {
        if (preg_match('/[^0-9ABCDEFabcdef]/', $hex[1])) {
            return $hex[0];
        }
        $color = $hex[1];
        $l = strlen($color);
        if ($l != 3 and $l != 6) {
            return $hex[0];
        }
        $l = $l / 3;
        return 'rgb(' . (hexdec(substr($color, 0, 1 * $l))) . ', ' . (hexdec(substr($color, 1 * $l, 1 * $l))) . ', ' . (hexdec(substr($color, 2 * $l, 1 * $l))) . ');';
    }

    /**
     * Request::unhtmlentities()
     *
     * @param mixed $value
     * @return
     */
    private function unhtmlentities($value)
    {
        $value = preg_replace("/%3A%2F%2F/", '', $value); // :// to empty
        $value = preg_replace('/([\x00-\x08][\x0b-\x0c][\x0e-\x20])/', '', $value);
        $value = preg_replace("/%u0([a-z0-9]{3})/i", "&#x\\1;", $value);
        $value = preg_replace("/%([a-z0-9]{2})/i", "&#x\\1;", $value);
        $value = str_ireplace(['&#x53;&#x43;&#x52;&#x49;&#x50;&#x54;', '&#x26;&#x23;&#x78;&#x36;&#x41;&#x3B;&#x26;&#x23;&#x78;&#x36;&#x31;&#x3B;&#x26;&#x23;&#x78;&#x37;&#x36;&#x3B;&#x26;&#x23;&#x78;&#x36;&#x31;&#x3B;&#x26;&#x23;&#x78;&#x37;&#x33;&#x3B;&#x26;&#x23;&#x78;&#x36;&#x33;&#x3B;&#x26;&#x23;&#x78;&#x37;&#x32;&#x3B;&#x26;&#x23;&#x78;&#x36;&#x39;&#x3B;&#x26;&#x23;&#x78;&#x37;&#x30;&#x3B;&#x26;&#x23;&#x78;&#x37;&#x34;&#x3B;', '/*', '*/', '<!--', '-->', '<!-- -->', '&#x0A;', '&#x0D;', '&#x09;', ''], '', $value);

        $search = '/&#[xX]0{0,8}(21|22|23|24|25|26|27|28|29|2a|2b|2d|2f|30|31|32|33|34|35|36|37|38|39|3a|3b|3d|3f|40|41|42|43|44|45|46|47|48|49|4a|4b|4c|4d|4e|4f|50|51|52|53|54|55|56|57|58|59|5a|5b|5c|5d|5e|5f|60|61|62|63|64|65|66|67|68|69|6a|6b|6c|6d|6e|6f|70|71|72|73|74|75|76|77|78|79|7a|7b|7c|7d|7e);?/i';
        $value = preg_replace_callback($search, [$this, 'chr_hexdec_callback'], $value);

        $search = '/&#0{0,8}(33|34|35|36|37|38|39|40|41|42|43|45|47|48|49|50|51|52|53|54|55|56|57|58|59|61|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|126);?/i';
        $value = preg_replace_callback($search, [$this, 'chr_callback'], $value);

        $search = ['&#60', '&#060', '&#0060', '&#00060', '&#000060', '&#0000060', '&#60;', '&#060;', '&#0060;', '&#00060;', '&#000060;', '&#0000060;', '&#x3c', '&#x03c', '&#x003c', '&#x0003c', '&#x00003c', '&#x000003c', '&#x3c;', '&#x03c;', '&#x003c;', '&#x0003c;', '&#x00003c;', '&#x000003c;', '&#X3c', '&#X03c', '&#X003c', '&#X0003c', '&#X00003c', '&#X000003c', '&#X3c;', '&#X03c;', '&#X003c;', '&#X0003c;', '&#X00003c;', '&#X000003c;', '&#x3C', '&#x03C', '&#x003C', '&#x0003C', '&#x00003C', '&#x000003C', '&#x3C;', '&#x03C;', '&#x003C;', '&#x0003C;', '&#x00003C;', '&#x000003C;', '&#X3C', '&#X03C', '&#X003C', '&#X0003C', '&#X00003C', '&#X000003C', '&#X3C;', '&#X03C;', '&#X003C;', '&#X0003C;', '&#X00003C;', '&#X000003C;', '\x3c', '\x3C', '\u003c', '\u003C'];
        $value = str_ireplace($search, '<', $value);

        return $value;
    }

    /**
     * @param string[] $attrSet
     * @param string $tagName
     * @param boolean $isvalid
     * @return string[]
     */
    private function filterAttr($attrSet, $tagName, &$isvalid)
    {
        $newSet = [];

        for ($i = 0, $count = sizeof($attrSet); $i < $count; ++$i) {
            if (!$attrSet[$i]) {
                continue;
            }
            $attrSubSet = array_map('trim', explode('=', trim($attrSet[$i]), 2));
            $attrSubSet[0] = strtolower($attrSubSet[0]);

            if (!preg_match('/[a-z]+/i', $attrSubSet[0]) or in_array($attrSubSet[0], $this->disabledattributes) or preg_match('/^on/i', $attrSubSet[0])) {
                continue;
            }

            if (!empty($attrSubSet[1])) {
                $attrSubSet[1] = preg_replace('/[ ]+/', ' ', $attrSubSet[1]);
                $attrSubSet[1] = preg_replace("/^\"(.*)\"$/", "\\1", $attrSubSet[1]);
                $attrSubSet[1] = preg_replace("/^\'(.*)\'$/", "\\1", $attrSubSet[1]);
                $attrSubSet[1] = str_replace(['"', '&quot;'], "'", $attrSubSet[1]);

                // Security check Data URLs
                if (preg_match('/^[\r\n\s\t]*d\s*a\s*t\s*a\s*\:([^\,]*?)\;*(base64)*?[\r\n\s\t]*\,[\r\n\s\t]*(.*?)[\r\n\s\t]*$/isu', $attrSubSet[1], $m)) {
                    if (empty($m[2])) {
                        $dataURLs = urldecode($m[3]);
                    } else {
                        $dataURLs = (string) base64_decode($m[3]);
                    }

                    $checkValid = true;
                    $this->filterTags($dataURLs, $checkValid);
                    if (!$checkValid) {
                        continue;
                    }
                }

                $value = $this->unhtmlentities($attrSubSet[1]);
                $search = [
                    'javascript' => '/j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t/si',
                    'vbscript' => '/v\s*b\s*s\s*c\s*r\s*i\s*p\s*t/si',
                    'script' => '/s\s*c\s*r\s*i\s*p\s*t/si',
                    'applet' => '/a\s*p\s*p\s*l\s*e\s*t/si',
                    'alert' => '/a\s*l\s*e\s*r\s*t/si',
                    'document' => '/d\s*o\s*c\s*u\s*m\s*e\s*n\s*t/si',
                    'write' => '/w\s*r\s*i\s*t\s*e/si',
                    'cookie' => '/c\s*o\s*o\s*k\s*i\s*e/si',
                    'window' => '/w\s*i\s*n\s*d\s*o\s*w/si',
                    'data:' => '/d\s*a\s*t\s*a\s*\:/si'
                ];
                $value = preg_replace(array_values($search), array_keys($search), $value);

                // Giới hạn link từ các tên miền bên ngoài
                if ($this->isRestrictDomain and isset($this->remoteAttrCheck[$attrSubSet[0]]) and in_array($tagName, $this->remoteAttrCheck[$attrSubSet[0]])) {
                    $url_info = parse_url($value);
                    if (isset($url_info['host'])) {
                        $domain = $url_info['host'];
                        $callBack = function ($domain_allowed) use ($domain) {
                            return preg_match('/^' . preg_quote($domain, '/') . '$/iu', $domain_allowed);
                        };
                        if (!array_filter($this->validDomains, $callBack)) {
                            continue;
                        }
                    }
                }

                // Security remove object param tag
                if ('param' == $tagName and 'name' == $attrSubSet[0] and preg_match('/^[\r\n\s\t]*(allowscriptaccess|allownetworking)/isu', strtolower($value))) {
                    return [];
                }
                if (preg_match('/(expression|javascript|behaviour|vbscript|mocha|livescript)(\:*)/', $value)) {
                    continue;
                }
                if (!empty($this->disablecomannds) and preg_match('#(' . implode('|', $this->disablecomannds) . ')(\s*)\((.*?)\)#si', $value)) {
                    continue;
                }

                $attrSubSet[1] = preg_replace_callback('/\#([0-9ABCDEFabcdef]{3,6})[\;]*/', [$this, 'color_hex2rgb_callback'], $attrSubSet[1]);
            } elseif ($attrSubSet[1] !== '0') {
                $attrSubSet[1] = $attrSubSet[0];
            }
            $newSet[] = $attrSubSet[0] . '=[@{' . $attrSubSet[1] . '}@]';
        }

        if ($tagName == 'embed') {
            $newSet[] = 'allowscriptaccess=[@{never}@]';
            $newSet[] = 'allownetworking=[@{internal}@]';
        }

        return $newSet;
    }

    /**
     * @param string $source
     * @param boolean $isvalid
     * @return string
     */
    private function filterTags($source, &$isvalid = true)
    {
        $checkInvalid = 0;
        $source = preg_replace('/\<script([^\>]*)\>(.*)\<\/script\>/isU', '', $source, -1, $checkInvalid);
        if ($checkInvalid > 0) {
            $isvalid = false;
        }

        $preTag = null;
        $postTag = $source;
        $tagOpen_start = strpos($source, '<');

        while ($tagOpen_start !== false) {
            $preTag .= substr($postTag, 0, $tagOpen_start);
            $postTag = substr($postTag, $tagOpen_start);
            $fromTagOpen = substr($postTag, 1);
            $tagOpen_end = strpos($fromTagOpen, '>');

            if ($tagOpen_end === false) {
                break;
            }

            $tagOpen_nested = strpos($fromTagOpen, '<');

            if (($tagOpen_nested !== false) and ($tagOpen_nested < $tagOpen_end)) {
                $preTag .= substr($postTag, 0, ($tagOpen_nested + 1));
                $postTag = substr($postTag, ($tagOpen_nested + 1));
                $tagOpen_start = strpos($postTag, '<');
                continue;
            }

            $tagOpen_nested = (strpos($fromTagOpen, '<') + $tagOpen_start + 1);
            $currentTag = substr($fromTagOpen, 0, $tagOpen_end);
            $tagLength = strlen($currentTag);

            if (!$tagOpen_end) {
                $preTag .= $postTag;
                $tagOpen_start = strpos($postTag, '<');
            }

            $tagLeft = $currentTag;
            $attrSet = [];
            $currentSpace = strpos($tagLeft, ' ');

            if (substr($currentTag, 0, 1) == '/') {
                $isCloseTag = true;
                list($tagName) = explode(' ', $currentTag);
                $tagName = strtolower(substr($tagName, 1));
            } else {
                $isCloseTag = false;
                list($tagName) = explode(' ', $currentTag);
                $tagName = strtolower($tagName);
            }

            if ((!preg_match('/^[a-z][a-z0-9]*$/i', $tagName)) or in_array($tagName, $this->disabletags)) {
                $postTag = substr($postTag, ($tagLength + 2));
                $tagOpen_start = strpos($postTag, '<');
                $isvalid = false;
                continue;
            }

            while ($currentSpace !== false) {
                $fromSpace = substr($tagLeft, ($currentSpace + 1));
                $nextSpace = strpos($fromSpace, ' ');
                $openQuotes = strpos($fromSpace, '"');
                $closeQuotes = strpos(substr($fromSpace, ($openQuotes + 1)), '"') + $openQuotes + 1;

                if (strpos($fromSpace, '=') !== false) {
                    if (($openQuotes !== false) and (strpos(substr($fromSpace, ($openQuotes + 1)), '"') !== false)) {
                        $attr = substr($fromSpace, 0, ($closeQuotes + 1));
                    } else {
                        $attr = substr($fromSpace, 0, $nextSpace);
                    }
                } else {
                    $attr = substr($fromSpace, 0, $nextSpace);
                }

                if (!$attr) {
                    $attr = $fromSpace;
                }

                $attrSet[] = $attr;
                $tagLeft = substr($fromSpace, strlen($attr));
                $currentSpace = strpos($tagLeft, ' ');
            }

            if (!$isCloseTag) {
                if (!empty($attrSet)) {
                    $attrSet = $this->filterAttr($attrSet, $tagName, $isvalid);
                }
                if (!('param' == $tagName and empty($attrSet))) {
                    $preTag .= '{@[' . $tagName;
                    if (!empty($attrSet)) {
                        $preTag .= ' ' . implode(' ', $attrSet);
                    }
                    $preTag .= (strpos($fromTagOpen, '</' . $tagName)) ? ']@}' : ' /]@}';
                    if ($tagName == 'object') {
                        if (preg_match('/\]\@\}([\s]+)\{\@\[' . $tagName . '/', $preTag, $m)) {
                            $space = $m[1] . '    ';
                        } else {
                            $space = "\n    ";
                        }
                        $preTag .= $space . "{@[param name=[@{allowscriptaccess}@] value=[@{never}@] /]@}" . $space . "{@[param name=[@{allownetworking}@] value=[@{internal}@] /]@}\n";
                    }
                }
            } else {
                $preTag .= '{@[/' . $tagName . ']@}';
            }

            $postTag = substr($postTag, ($tagLength + 2));
            $tagOpen_start = strpos($postTag, '<');
        }

        $preTag .= $postTag;
        $preTag = str_replace(["'", '"', '<', '>'], ["&#039;", "&quot;", "&lt;", "&gt;"], $preTag);
        return trim(str_replace(["[@{", "}@]", "{@[", "]@}"], ['"', '"', "<", '>'], $preTag));
    }

    /**
     * Request::security_get()
     *
     * @param mixed $value
     * @return
     */
    private function security_get($value, $decode = false)
    {
        if (is_array($value)) {
            $keys = array_keys($value);
            foreach ($keys as $key) {
                $value[$key] = $this->security_get($value[$key], $decode);
            }
        } else {
            if (!empty($value) and !is_numeric($value)) {
                if ($decode == true) {
                    $value = urldecode($value);
                }

                $value = str_replace(["\t", "\r", "\n", "../"], "", $value);
                $value = $this->unhtmlentities($value);
                unset($matches);
                preg_match_all('/<!\[cdata\[(.*?)\]\]>/is', $value, $matches);
                $value = str_replace($matches[0], $matches[1], $value);
                $value = strip_tags($value);
                $value = preg_replace('#(' . implode('|', $this->disablecomannds) . ')(\s*)\((.*?)\)#si', "", $value);
                $value = str_replace(["'", '"', '<', '>'], ["&#039;", "&quot;", "&lt;", "&gt;"], $value);
                $value = trim($value);
            }
        }
        return $value;
    }

    /**
     * Request::security_post()
     *
     * @param mixed $value
     * @return
     */
    public function security_post($value)
    {
        if (is_array($value)) {
            $keys = array_keys($value);
            foreach ($keys as $key) {
                $value[$key] = $this->security_post($value[$key]);
            }
        } else {
            // Fix block tag
            $value = str_replace(['[', ']'], ['&#91;', '&#93;'], $value);

            if (preg_match_all('/<!\[cdata\[(.*?)\]\]>/is', $value, $matches)) {
                $value = str_replace($matches[0], $matches[1], $value);
            }
            $value = $this->filterTags($value);
        }
        return $value;
    }

    /**
     * Request::security_cookie()
     *
     * @param mixed $value
     * @return
     */
    private function security_cookie($value)
    {
        return $value;
    }

    /**
     * Request::security_session()
     *
     * @param mixed $value
     * @return
     */
    private function security_session($value)
    {
        return $value;
    }

    /**
     * Request::parse_mode()
     *
     * @param mixed $mode
     * @return
     */
    private function parse_mode($mode)
    {
        if (empty($mode)) {
            return [$this->request_default_mode];
        }
        $mode = explode(',', $mode);
        $mode = array_map('trim', $mode);
        $mode = array_map('strtolower', $mode);
        $mode = array_intersect($this->allow_request_mods, $mode);
        if (empty($mode)) {
            return [$this->request_default_mode];
        }
        return array_values($mode);
    }

    /**
     * Request::encodeCookie()
     *
     * @param mixed $string
     * @return
     */
    private function encodeCookie($string)
    {
        $iv = substr($this->cookie_key, 0, 16);
        $string = openssl_encrypt($string, 'aes-256-cbc', $this->cookie_key, 0, $iv);
        return strtr($string, '+/=', '-_,');
    }

    /**
     * Request::decodeCookie()
     *
     * @param mixed $string
     * @return
     */
    private function decodeCookie($string)
    {
        $string = strtr($string, '-_,', '+/=');
        $iv = substr($this->cookie_key, 0, 16);
        return openssl_decrypt($string, 'aes-256-cbc', $this->cookie_key, 0, $iv);
    }

    /**
     * Request::get_value()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $default
     * @param bool $decode
     * @param bool $filter
     * @return
     */
    private function get_value($name, $mode = null, $default = null, $decode = true, $filter = true)
    {
        $modes = $this->parse_mode($mode);
        foreach ($modes as $mode) {
            switch ($mode) {
                case 'get':
                    if (array_key_exists($name, $_GET)) {
                        $value = $_GET[$name];
                        if (empty($value) or is_numeric($value)) {
                            return $value;
                        }
                        return ($filter == true) ? $this->security_get($value) : $value;
                    }
                    break;
                case 'post':
                    if (array_key_exists($name, $_POST)) {
                        $value = $_POST[$name];
                        if (empty($value) or is_numeric($value)) {
                            return $value;
                        }
                        return ($filter == true) ? $this->security_post($value) : $value;
                    }
                    break;
                case 'cookie':
                    if (array_key_exists($this->cookie_prefix . '_' . $name, $_COOKIE)) {
                        $value = $_COOKIE[$this->cookie_prefix . '_' . $name];
                        if ($decode) {
                            $value = $this->decodeCookie($value);
                        }
                        if (empty($value) or is_numeric($value)) {
                            return $value;
                        }
                        return ($filter == true) ? $this->security_cookie($value) : $value;
                    }
                    break;
                case 'session':
                    if (array_key_exists($this->session_prefix . '_' . $name, $_SESSION)) {
                        $value = $_SESSION[$this->session_prefix . '_' . $name];
                        if ($decode) {
                            $value = $this->decodeCookie($value);
                        }
                        if (empty($value) or is_numeric($value)) {
                            return $value;
                        }
                        return ($filter == true) ? $this->security_session($value) : $value;
                    }
                    break;
                case 'request':
                    if (array_key_exists($name, $_POST)) {
                        $value = $_POST[$name];
                        if (empty($value) or is_numeric($value)) {
                            return $value;
                        }
                        return ($filter == true) ? $this->security_post($value) : $value;
                    } elseif (array_key_exists($name, $_GET)) {
                        $value = $_GET[$name];
                        if (empty($value) or is_numeric($value)) {
                            return $value;
                        }
                        return ($filter == true) ? $this->security_get($value) : $value;
                    }
                    break;
                case 'env':
                    if (array_key_exists($name, $_ENV)) {
                        $value = $_ENV[$name];
                        return $value;
                    }
                    break;
                case 'server':
                    if (array_key_exists($name, $_SERVER)) {
                        $value = $_SERVER[$name];
                        return $value;
                    }
                    break;
            }
        }
        return $default;
    }

    /**
     * Request::set_Cookie()
     *
     * @param mixed $name
     * @param string $value
     * @param integer $expire
     * @param bool $encode
     * @return
     */
    public function set_Cookie($name, $value = '', $expire = 0, $encode = true)
    {
        if (is_array($value)) {
            return false;
        }
        if (empty($name)) {
            return false;
        }
        $name = $this->cookie_prefix . '_' . $name;
        if ($encode) {
            $value = $this->encodeCookie($value);
        }
        $expire = intval($expire);
        if (!empty($expire)) {
            $expire += NV_CURRENTTIME;
        }

        return setcookie($name, $value, $expire, $this->cookie_path, $this->cookie_domain, $this->secure, $this->httponly);
    }

    /**
     * Request::set_Session()
     *
     * @param mixed $name
     * @param string $value
     * @return
     */
    public function set_Session($name, $value = '')
    {
        if (is_array($value)) {
            return false;
        }
        if (empty($name)) {
            return false;
        }
        $name = $this->session_prefix . '_' . $name;
        $value = $this->encodeCookie($value);
        $_SESSION[$name] = $value;
        return true;
    }

    /**
     * Request::unset_request()
     *
     * @param mixed $names
     * @param mixed $mode
     * @return
     */
    public function unset_request($names, $mode)
    {
        if (empty($names)) {
            return [];
        }
        $names = ',' . $names;
        unset($matches);
        preg_match_all("/\,\s*([a-zA-Z\_]{1}[a-zA-Z0-9\_]*)/", $names, $matches);
        $names = $matches[1];
        if (empty($names)) {
            return false;
        }
        $mode = $this->parse_mode($mode);
        foreach ($mode as $arr) {
            if ($arr == 'get') {
                foreach ($names as $name) {
                    if (empty($name)) {
                        continue;
                    }
                    unset($_GET[$name]);
                }
            } elseif ($arr == 'post') {
                foreach ($names as $name) {
                    if (empty($name)) {
                        continue;
                    }
                    unset($_POST[$name]);
                }
            } elseif ($arr == 'cookie') {
                foreach ($names as $name) {
                    if (empty($name)) {
                        continue;
                    }
                    $name2 = $this->cookie_prefix . '_' . $name;
                    if (!isset($_COOKIE[$name2])) {
                        continue;
                    }
                    $expire = NV_CURRENTTIME - 3600;

                    setcookie($name2, '', $expire, $this->cookie_path, $this->cookie_domain, $this->secure, $this->httponly);

                    unset($_COOKIE[$name2]);
                }
            } elseif ($arr == 'session') {
                foreach ($names as $name) {
                    if (empty($name)) {
                        continue;
                    }
                    $name2 = $this->session_prefix . '_' . $name;
                    if (!isset($_SESSION[$name2])) {
                        continue;
                    }
                    unset($_SESSION[$name2]);
                }
            } elseif ($arr == 'request') {
                foreach ($names as $name) {
                    if (empty($name)) {
                        continue;
                    }
                    unset($_REQUEST[$name]);
                }
            } elseif ($arr == 'env') {
                foreach ($names as $name) {
                    if (empty($name)) {
                        continue;
                    }
                    unset($_ENV[$name]);
                }
            } elseif ($arr == 'server') {
                foreach ($names as $name) {
                    if (empty($name)) {
                        continue;
                    }
                    unset($_SERVER[$name]);
                }
            }
        }
    }

    /**
     * Request::isset_request()
     *
     * @param mixed $names
     * @param mixed $mode
     * @param bool $all
     * @return
     */
    public function isset_request($names, $mode, $all = true)
    {
        if (empty($names)) {
            return [];
        }
        $names = ',' . $names;
        unset($matches);
        preg_match_all("/\,\s*([a-zA-Z\_]{1}[a-zA-Z0-9\_]*)/", $names, $matches);
        $names = $matches[1];
        if (empty($names)) {
            return false;
        }
        $names = array_flip($names);
        $mode = $this->parse_mode($mode);
        foreach ($mode as $arr) {
            $array_keys = array_keys($names);
            foreach ($array_keys as $name) {
                if ($arr == 'get' and isset($_GET[$name])) {
                    if (empty($all)) {
                        return true;
                    }
                    unset($names[$name]);
                } elseif ($arr == 'post' and isset($_POST[$name])) {
                    if (empty($all)) {
                        return true;
                    }
                    unset($names[$name]);
                } elseif ($arr == 'cookie' and isset($_COOKIE[$this->cookie_prefix . '_' . $name])) {
                    if (empty($all)) {
                        return true;
                    }
                    unset($names[$name]);
                } elseif ($arr == 'session' and isset($_SESSION[$this->session_prefix . '_' . $name])) {
                    if (empty($all)) {
                        return true;
                    }
                    unset($names[$name]);
                } elseif ($arr == 'request' and isset($_REQUEST[$name])) {
                    if (empty($all)) {
                        return true;
                    }
                    unset($names[$name]);
                } elseif ($arr == 'env' and isset($_ENV[$name])) {
                    if (empty($all)) {
                        return true;
                    }
                    unset($names[$name]);
                } elseif ($arr == 'server' and isset($_SERVER[$name])) {
                    if (empty($all)) {
                        return true;
                    }
                    unset($names[$name]);
                }
            }
        }
        if (!empty($names)) {
            return false;
        }
        return true;
    }

    /**
     * Request::get_bool()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $default
     * @param bool $decode
     * @param bool $filter
     * @return
     */
    public function get_bool($name, $mode = null, $default = null, $decode = true, $filter = true)
    {
        return (bool) $this->get_value($name, $mode, $default, $decode, $filter);
    }

    /**
     * Request::get_int()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $default
     * @param bool $decode
     * @param bool $filter
     * @return
     */
    public function get_int($name, $mode = null, $default = null, $decode = true, $filter = true)
    {
        return (int) $this->get_value($name, $mode, $default, $decode, $filter);
    }

    /**
     * @since 4.3.08
     *
     * @param string $name
     * @param string $mode
     * @param int $default
     * @param boolean $decode
     * @param boolean $filter
     * @return number
     */
    public function get_absint($name, $mode = null, $default = null, $decode = true, $filter = true)
    {
        return abs(intval($this->get_value($name, $mode, $default, $decode, $filter)));
    }

    /**
     * Request::get_float()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $default
     * @param bool $decode
     * @param bool $filter
     * @return
     */
    public function get_float($name, $mode = null, $default = null, $decode = true, $filter = true)
    {
        return (float) $this->get_value($name, $mode, $default, $decode, $filter);
    }

    /**
     * Request::get_string()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $default
     * @param bool $decode
     * @param bool $filter
     * @return
     */
    public function get_string($name, $mode = null, $default = null, $decode = true, $filter = true)
    {
        return (string) $this->get_value($name, $mode, $default, $decode, $filter);
    }

    /**
     * Request::_get_title()
     *
     * @param mixed $value
     * @param mixed $specialchars
     * @param mixed $preg_replace
     * @return
     */
    private function _get_title($value, $specialchars, $preg_replace)
    {
        $value = strip_tags($value);
        if (( bool )$specialchars == true) {
            $search = ['&', '\'', '"', '<', '>', '\\', '/', '(', ')', '*', '[', ']', '!', '=', '%', '^', ':', '{', '}', '`', '~'];
            $replace = ['&amp;', '&#039;', '&quot;', '&lt;', '&gt;', '&#x005C;', '&#x002F;', '&#40;', '&#41;', '&#42;', '&#91;', '&#93;', '&#33;', '&#x3D;', '&#x25;', '&#x5E;', '&#x3A;', '&#x7B;', '&#x7D;', '&#x60;', '&#x7E;'];

            $value = str_replace($replace, $search, $value);
            $value = str_replace("&#x23;", "#", $value);
            $value = str_replace($search, $replace, $value);
            $value = preg_replace("/([^\&]+)\#/", "\\1&#x23;", $value);
        }

        if (!empty($preg_replace)) {
            if (isset($preg_replace['pattern']) and !empty($preg_replace['pattern']) and isset($preg_replace['replacement'])) {
                $value = preg_replace($preg_replace['pattern'], $preg_replace['replacement'], $value);
            }
        }
        return trim($value);
    }

    /**
     * Request::get_title()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $default
     * @param bool $specialchars
     * @param mixed $preg_replace
     * @param bool $filter
     * @return
     */
    public function get_title($name, $mode = null, $default = null, $specialchars = false, $preg_replace = [], $filter = true)
    {
        $value = (string) $this->get_value($name, $mode, $default, true, $filter);
        return $this->_get_title($value, $specialchars, $preg_replace);
    }

    /**
     * Request::_get_editor()
     *
     * @param mixed $value
     * @param mixed $allowed_html_tags
     * @return
     */
    private function _get_editor($value, $allowed_html_tags)
    {
        if (!empty($allowed_html_tags)) {
            $allowed_html_tags = array_map('trim', explode(',', $allowed_html_tags));
            $allowed_html_tags = '<' . implode('><', $allowed_html_tags) . '>';
            $value = strip_tags($value, $allowed_html_tags);
        }
        return trim($value);
    }

    /**
     * Request::get_editor()
     *
     * @param mixed $name
     * @param mixed $default
     * @param bool $allowed_html_tags
     * @param mixed $save
     * @param bool $filter
     * @return
     */
    public function get_editor($name, $default = '', $allowed_html_tags = '', $filter = true)
    {
        $value = ( string )$this->get_value($name, 'post', $default, true, $filter);
        return $this->_get_editor($value, $allowed_html_tags);
    }

    /**
     * Request::_get_textarea()
     *
     * @param mixed $value
     * @param mixed $allowed_html_tags
     * @param mixed $save
     * @return
     */
    private function _get_textarea($value, $allowed_html_tags, $save)
    {
        if (!empty($allowed_html_tags)) {
            $allowed_html_tags = array_map('trim', explode(',', $allowed_html_tags));
            $allowed_html_tags = '<' . implode('><', $allowed_html_tags) . '>';
            $value = strip_tags($value, $allowed_html_tags);
        }
        if ((bool) $save) {
            $value = strtr($value, [
                "\r\n" => '<br />',
                "\r" => '<br />',
                "\n" => '<br />'
            ]);
        }
        return trim($value);
    }

    /**
     * Request::get_textarea()
     *
     * @param mixed $name
     * @param mixed $default
     * @param bool $allowed_html_tags
     * @param mixed $save
     * @param bool $filter
     * @return
     */
    public function get_textarea($name, $default = '', $allowed_html_tags = '', $save = false, $filter = true)
    {
        $value = (string)$this->get_value($name, 'post', $default, true, $filter);
        return $this->_get_textarea($value, $allowed_html_tags, $save);
    }

    /**
     * Request::get_array()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $default
     * @param bool $decode
     * @param bool $filter
     * @return
     */
    public function get_array($name, $mode = null, $default = null, $decode = true, $filter = true)
    {
        return (array) $this->get_value($name, $mode, $default, $decode, $filter);
    }

    /**
     * Request::get_typed_array()
     *
     * @param mixed $name
     * @param mixed $mode
     * @param mixed $type
     * @param mixed $default
     * @param bool $filter
     * @return
     */
    public function get_typed_array($name, $mode = null, $type = null, $default = null, $specialchars = false, $preg_replace = [], $allowed_html_tags = '', $save = false, $filter = true)
    {
        $arr = $this->get_array($name, $mode, $default, true, $filter);
        $array_keys = array_keys($arr);
        foreach ($array_keys as $key) {
            switch ($type) {
                case 'bool':
                    $arr[$key] = (bool) $arr[$key];
                    break;
                case 'int':
                    $arr[$key] = (int) $arr[$key];
                    break;
                case 'float':
                    $arr[$key] = (float) $arr[$key];
                    break;
                case 'string':
                    $arr[$key] = (string) $arr[$key];
                    break;
                case 'array':
                    $arr[$key] = (array) $arr[$key];
                    break;
                case 'title':
                    $arr[$key] = (string) $this->_get_title($arr[$key], $specialchars, $preg_replace);
                    break;
                case 'textarea':
                    $arr[$key] = (string) $this->_get_textarea($arr[$key], $allowed_html_tags, $save);
                    break;
                case 'editor':
                    $arr[$key] = (string) $this->_get_editor($arr[$key], $allowed_html_tags);
            }
        }
        return $arr;
    }

    /**
     * @return string|NULL
     */
    private function getAllowOriginHeaderValue()
    {
        // Không block hoặc domain hợp lệ (domain trong danh sách hoặc là self)
        if (!$this->restrictCrossDomain or $this->origin_key === 1 or in_array($this->origin, $this->validCrossDomains)) {
            $this->isOriginValid = true;
            return $this->origin;
        }

        $this->isOriginValid = false;
        return $this->my_current_domain;
    }
}
