<?php

// If this file is called directly, abort.
defined('ABSPATH') || die('No direct access!');

class Dragonizer_SSL
{
    private static $options;
    private $http_urls = array();

    public function __construct($options)
    {
        if (empty($options) || !empty(self::$options)) {
            return;
        }

        $default = [
            'http_headers'          => [
                'enable_hsts'           => false,
                'x_content_options'     => false,
                'referrer_policy'       => false,
                'x_frame_options'       => false,
                'block_popups'          => false,
                'sharing_resources'     => false,
                'embedder_Policy'       => false,
                'security_Policy'       => false,
                'cross_Domain_Policies' => false,
                'permissions_policy'    =>
                [
                    'status' => false,
                    'values' => []
                ]
            ],
            'htaccess_php'          => '',
            'nginx_php'             => '',
            'php_redirect'          => false,
            'fix_backend_mixed'     => false,
            'fix_frontend_mixed'    => false,
            'htaccess_remove_www'   => false,
        ];

        self::$options = empty($options) ? $default : dr_fix_settings($default, $options);
    }

    private function settings(): object
    {
        return is_object(self::$options) ? self::$options : (object)self::$options;
    }

    public function init()
    {
        // if ($this->settings()->php_redirect) {
        //     add_action('wp', array($this, 'wp_redirect_to_ssl'), 10, 3);
        // }

        if (
            $this->settings()->fix_backend_mixed
        ) {
            add_filter('upload_dir', array($this, 'replace_https'));
            add_filter('option_siteurl', array($this, 'replace_https'));
            add_filter('option_home', array($this, 'replace_https'));
            add_filter('option_url', array($this, 'replace_https'));
            add_filter('option_wpurl', array($this, 'replace_https'));
            add_filter('option_stylesheet_url', array($this, 'replace_https'));
            add_filter('option_template_url', array($this, 'replace_https'));
            add_filter('wp_get_attachment_url', array($this, 'replace_https'));
            add_filter('widget_text', array($this, 'replace_https'));
            add_filter('login_url', array($this, 'replace_https'));
            add_filter('language_attributes', array($this, 'replace_https'));
        }
    }

    public function wp_redirect_to_ssl()
    {
        // if (!dr_is_ssl()) {
        //     $redirect_url = "https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        //     dr_redirect($redirect_url);
        //     exit;
        // }
    }

    public function htaccess_remove_www(string $tab): string
    {
        $new_rules = "\n# Remove www from url\n";
        $new_rules .= "<IfModule mod_rewrite.c>\n";
        $new_rules .= $tab . "RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]\n";
        $new_rules .= $tab . "RewriteRule ^ %{REQUEST_SCHEME}://%1%{REQUEST_URI} [R=301,L]\n";
        $new_rules .= "</IfModule>\n";

        return $new_rules;
    }

    public function add_htaccess_redirect(string $tab): string
    {
        $new_rules = "\n# Force HTTPS\n";
        $new_rules .= "<IfModule mod_rewrite.c>\n";
        $new_rules .= $tab . "RewriteCond %{HTTPS} !=on [NC]\n";
        if ($this->has_acme_challenge_directory()) {
            $new_rules .= $tab . "RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/\n";
        }
        $new_rules .= $tab . "RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]\n";
        $new_rules .= "</IfModule>\n";

        return $new_rules;
    }

    public function add_security_headers_nginx()
    {
        //
    }

    public function add_security_headers_htaccess(string $tab): string
    {
        $https_home_url = get_home_url(false, '', 'https');
        $domain = filter_var(parse_url($https_home_url, PHP_URL_HOST), FILTER_SANITIZE_STRING);
        $new_rules = "";

        // Basic Access Control Headers
        $new_rules .= $tab . "Header set Access-Control-Allow-Methods \"GET,POST\"\n";
        $new_rules .= $tab . "Header set Access-Control-Allow-Headers \"Content-Type, Authorization\"\n";

        // Content Security Policy
        if ($this->settings()->http_headers->security_Policy) {
            $new_rules .= $tab . "Header set Content-Security-Policy \"upgrade-insecure-requests;\"\n";
            $new_rules .= $tab . "Header set X-Content-Security-Policy \"default-src 'self'; img-src *; media-src * data:;\"\n";
        }

        // Cross-Origin Policies
        if ($this->settings()->http_headers->embedder_Policy) {
            $new_rules .= $tab . "Header set Cross-Origin-Embedder-Policy \"unsafe-none; report-to='default'\"\n";
            $new_rules .= $tab . "Header set Cross-Origin-Embedder-Policy-Report-Only \"unsafe-none; report-to='default'\"\n";
        }

        if ($this->settings()->http_headers->block_popups) {
            $new_rules .= $tab . "Header set Cross-Origin-Opener-Policy \"unsafe-none\"\n";
            $new_rules .= $tab . "Header set Cross-Origin-Opener-Policy-Report-Only \"unsafe-none; report-to='default'\"\n";
        }

        if ($this->settings()->http_headers->sharing_resources) {
            // Allow both main domain and CDN subdomain
            $new_rules .= $tab . "Header set Access-Control-Allow-Origin \"https://$domain\"\n";
            $new_rules .= $tab . "Header set Cross-Origin-Resource-Policy \"cross-origin\"\n";
            $new_rules .= $tab . "Header set Vary \"Origin\"\n";
        } else {
            $new_rules .= $tab . "Header set Access-Control-Allow-Origin \"*\"\n";
            $new_rules .= $tab . "Header set Cross-Origin-Resource-Policy \"cross-origin\"\n";
        }

        // Permissions Policy with expanded features
        if ($this->settings()->http_headers->permissions_policy->status) {
            $new_rules .= $tab . stripslashes($this->permissions_policy_header(true)) . "\n";
        }

        // Referrer Policy
        if ($this->settings()->http_headers->referrer_policy) {
            $new_rules .= $tab . "Header set Referrer-Policy \"strict-origin-when-cross-origin\"\n";
        }

        // HSTS with improved security
        if ($this->settings()->http_headers->enable_hsts) {
            $new_rules .= $tab . "Header set Strict-Transport-Security \"max-age=63072000; includeSubDomains; preload\"\n";
        } else {
            $new_rules .= $tab . "Header set Strict-Transport-Security \"max-age=31536000;\"\n";
        }

        // Security Headers
        if ($this->settings()->http_headers->x_content_options) {
            $new_rules .= $tab . "Header set X-Content-Type-Options \"nosniff\"\n";
        }

        if ($this->settings()->http_headers->x_frame_options) {
            $new_rules .= $tab . "Header set X-Frame-Options \"SAMEORIGIN\"\n";
        }

        if ($this->settings()->http_headers->cross_Domain_Policies) {
            $new_rules .= $tab . "Header set X-Permitted-Cross-Domain-Policies \"none\"\n";
        }

        return $new_rules;
    }

    public function add_security_headers_php()
    {
        $tab = "\t";
        $https_home_url = get_home_url(false, '', 'https');
        $domain = filter_var(parse_url($https_home_url, PHP_URL_HOST), FILTER_SANITIZE_STRING);

        $headers = "// Set headers if they haven't been sent already\n";
        $headers .= "if (!headers_sent()) {\n";

        // Basic Access Control Headers
        $headers .= $tab . "header('Access-Control-Allow-Methods: GET,POST');\n";
        $headers .= $tab . "header('Access-Control-Allow-Headers: Content-Type, Authorization');\n";

        // Content Security Policy
        if ($this->settings()->http_headers->security_Policy) {
            $headers .= $tab . "header('Content-Security-Policy: upgrade-insecure-requests;');\n";
            $headers .= $tab . "header('X-Content-Security-Policy: default-src \"self\"; img-src *; media-src * data:;');\n";
        }

        // Cross-Origin Policies
        if ($this->settings()->http_headers->embedder_Policy) {
            $headers .= $tab . "header('Cross-Origin-Embedder-Policy: unsafe-none; report-to=\"default\"');\n";
            $headers .= $tab . "header('Cross-Origin-Embedder-Policy-Report-Only: unsafe-none; report-to=\"default\"');\n";
        }

        if ($this->settings()->http_headers->block_popups) {
            $headers .= $tab . "header('Cross-Origin-Opener-Policy: unsafe-none');\n";
            $headers .= $tab . "header('Cross-Origin-Opener-Policy-Report-Only: unsafe-none; report-to=\"default\"');\n";
        }

        if ($this->settings()->http_headers->sharing_resources) {
            // Allow both main domain and CDN subdomain
            $headers .= $tab . "header('Access-Control-Allow-Origin: https://$domain');\n";
            $headers .= $tab . "header('Cross-Origin-Resource-Policy: cross-origin');\n";
            $headers .= $tab . "header('Vary: Origin');\n";
        } else {
            $headers .= $tab . "header('Access-Control-Allow-Origin: *');\n";
            $headers .= $tab . "header('Cross-Origin-Resource-Policy: cross-origin');\n";
        }

        // Permissions Policy with expanded features
        if ($this->settings()->http_headers->permissions_policy->status) {
            $headers .= $tab . "header('" . $this->permissions_policy_header() . "');\n";
        }

        // Referrer Policy
        if ($this->settings()->http_headers->referrer_policy) {
            $headers .= $tab . "header('Referrer-Policy: strict-origin-when-cross-origin');\n";
        }

        // HSTS with improved security
        if ($this->settings()->http_headers->enable_hsts) {
            $headers .= $tab . "header('Strict-Transport-Security: max-age=63072000; includeSubDomains; preload');\n";
        } else {
            $headers .= $tab . "header('Strict-Transport-Security: max-age=31536000;');\n";
        }

        // Security Headers
        if ($this->settings()->http_headers->x_content_options) {
            $headers .= $tab . "header('X-Content-Type-Options: nosniff');\n";
        }

        if ($this->settings()->http_headers->x_frame_options) {
            $headers .= $tab . "header('X-Frame-Options: SAMEORIGIN');\n";
        }

        if ($this->settings()->http_headers->cross_Domain_Policies) {
            $headers .= $tab . "header('X-Permitted-Cross-Domain-Policies: none');\n";
        }

        $headers .= "}\n";

        return $headers;
    }

    private function permissions_policy_header($htaccess = false)
    {
        $permissions_policy_rules = $this->settings()->http_headers->permissions_policy->values;

        $rules = '';
        foreach ($permissions_policy_rules as $policy => $value) {
            switch ($value) {
                case 'self':
                    $rules .= $policy . "=(self)" . ", ";
                    break;
                case 'allowed':
                    $rules .= $policy . "=(*)" . ", ";
                    break;
            }
        }

        // Remove last space and , from string
        $rules = substr_replace($rules, "", -2);
        return $htaccess ? 'Header set Permissions-Policy "' . $rules . '"' : 'Permissions-Policy: ' . $rules;
    }

    private function can_fix_mixed_content()
    {
        switch (true) {
            case (defined('JSON_REQUEST') && JSON_REQUEST):
            case (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST):
                return false;
        }

        $this->build_url_list();

        return true;
    }

    private function build_url_list()
    {
        $home         = str_replace("https://", "http://", get_option('home'));
        $home_no_www  = str_replace("://www.", "://", $home);
        $home_yes_www = str_replace("://", "://www.", $home_no_www);
        $escaped_home = str_replace("/", "\/", $home);

        $this->http_urls = array(
            $home_yes_www,
            $home_no_www,
            $escaped_home,
            "src='http://",
            'src="http://',
        );
    }

    public function replace_insecure_links($str)
    {
        if (substr($str, 0, 5) == "<?xml") return $str;

        if (!$this->can_fix_mixed_content()) return $str;

        $search_array = $this->http_urls;
        $ssl_array = str_replace(array("http://", "http:\/\/"), array("https://", "https:\/\/"), $search_array);

        $str = str_replace($search_array, $ssl_array, $str);

        $pattern = array(
            '~url\([\'"]?\K(http://)(?=[^)]+)~i',
            '~<link [^>]*?href=[\'"]\K(http://)(?=[^\'"]+)~i',
            '~<meta property="og:image" [^>]*?content=[\'"]\K(http://)(?=[^\'"]+)~i',
            '~<form [^>]*?action=[\'"]\K(http://)(?=[^\'"]+)~i',
        );

        $str = preg_replace($pattern, 'https://', $str);
        $str = preg_replace_callback('/<img[^\>]*[^\>\S]+srcset=[\'"]\K((?:[^"\'\s,]+\s*(?:\s+\d+[wx])(?:,\s*)?)+)["\']/', array($this, 'replace_src_set'), $str);
        $str = str_replace("<body", '<body data-dragonizer-ssl="1"', $str);

        if ($this->settings()->htaccess_remove_www) {
            $str = preg_replace('#https://www\.#', 'https://', $str);
        }

        return $str;
    }

    private function replace_src_set($matches)
    {
        return str_replace("http://", "https://", $matches[0]);
    }

    public function replace_https($value = null)
    {
        if (!empty($value)) {
            if (dr_is_ssl()) {
                if (!is_array($value) && !is_object($value)) {
                    $value = preg_replace('|/+$|', '', $value);
                    $value = preg_replace('|http://|', 'https://', $value);

                    if ($this->settings()->htaccess_remove_www) {
                        $value = preg_replace('#https://www\.#', 'https://', $value);
                    }
                }
            }
        }

        return $value;
    }

    private function get_test_page_contents()
    {
        $filecontents = get_transient('rsssl_testpage');
        if (!$filecontents) {
            $testpage_url = trailingslashit($this->test_url()) . "app\partials\ssl-secure\ssl-test-page.php";
            $response = wp_remote_get($testpage_url);
            if (is_array($response)) {
                $filecontents = wp_remote_retrieve_body($response);
            }

            if (empty($filecontents)) {
                $filecontents = 'not-valid';
            }
            set_transient('rsssl_testpage', $filecontents, DAY_IN_SECONDS);
        }
        return $filecontents;
    }

    private function test_url()
    {
        $plugin_url = str_replace("http://", "https://", trailingslashit(DRAGONIZER_URL));
        $https_home_url = str_replace("http://", "https://", home_url());

        //in some case we get a relative url here, so we check that.
        //we compare to urls replaced to https, in case one of them is still on http.
        if ((strpos($plugin_url, "https://") === FALSE) &&
            (strpos($plugin_url, $https_home_url) === FALSE)
        ) {
            //make sure we do not have a slash at the start
            $plugin_url = ltrim($plugin_url, "/");
            $plugin_url = trailingslashit(home_url()) . $plugin_url;
        }

        //for subdomains or domain mapping situations, we have to convert the plugin_url from main site to the subdomain url.
        if (is_multisite() && !is_main_site(get_current_blog_id()) && !$this->is_multisite_subfolder_install()) {
            $mainsiteurl = trailingslashit(str_replace("http://", "https://", network_site_url()));
            $home = trailingslashit($https_home_url);
            $plugin_url = str_replace($mainsiteurl, $home, $plugin_url);
        }

        return $plugin_url;
    }

    private function getabs_path()
    {
        $path = ABSPATH;
        if ($this->is_subdirectory_install()) {
            $siteUrl = site_url();
            $homeUrl = home_url();
            $diff = str_replace($homeUrl, "", $siteUrl);
            $diff = trim($diff, "/");
            $pos = strrpos($path, $diff);
            if ($pos !== false) {
                $path = substr_replace($path, "", $pos, strlen($diff));
                $path = trim($path, "/");
                $path = "/" . $path . "/";
            }
        }

        return $path;
    }

    private function has_acme_challenge_directory()
    {
        $abs_path = $this->getabs_path();

        if (file_exists("$abs_path.well-known/acme-challenge")) {
            return true;
        }
        return false;
    }

    private function is_multisite_subfolder_install()
    {
        if (!is_multisite()) {
            return false;
        }
        //we check this manually, as the SUBDOMAIN_INSTALL constant of wordpress might return false for domain mapping configs
        $is_subfolder = false;
        $args = array(
            'number' => 5,
            'offset' => 0,
        );
        $sites = get_sites($args);
        foreach ($sites as $site) {
            switch_to_blog($site->blog_id);
            if ($this->is_subfolder(home_url())) {
                $is_subfolder = true;
            }
            restore_current_blog(); //switches back to previous blog, not current, so we have to do it each loop
            if ($is_subfolder) return true;
        }

        return false;
    }

    private function is_subfolder($domain)
    {
        //remove slashes of the http(s)
        $domain = preg_replace("/(http:\/\/|https:\/\/)/", "", $domain);
        if (strpos($domain, "/") !== FALSE) {
            return true;
        }
        return false;
    }

    private function is_subdirectory_install()
    {
        return strlen(site_url()) > strlen(home_url());
    }
}
