<?php
namespace Grav\Common;

use Grav\Common\Page\Page;
use Grav\Common\Page\Pages;

/**
 * The URI object provides information about the current URL
 *
 * @author RocketTheme
 * @license MIT
 */
class Uri
{
    const HOSTNAME_REGEX = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/';

    public $url;

    protected $basename;
    protected $base;
    protected $root;
    protected $bits;
    protected $extension;
    protected $host;
    protected $content_path;
    protected $path;
    protected $paths;
    protected $query;
    protected $params;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $name = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
        // Remove port from HTTP_HOST generated $name
        $name = Utils::substrToString($name, ':');

        // Validate the hostname
        $name = preg_match(Uri::HOSTNAME_REGEX, $name) ? $name : 'unknown';

        $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
        $uri  = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';

        $root_path = str_replace(' ', '%20', rtrim(substr($_SERVER['PHP_SELF'], 0, strpos($_SERVER['PHP_SELF'], 'index.php')), '/'));

        // set the base
        if (isset($_SERVER['HTTPS'])) {
            $base = (@$_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
        } else {
            $base = 'http://';
        }

        // add the sever name
        $base .= $name;

        // add the port of needed
        if ($port != '80' && $port != '443') {
            $base .= ":".$port;
        }

        // check if userdir in the path and workaround PHP bug with PHP_SELF
        if (strpos($uri, '/~') !== false && strpos($_SERVER['PHP_SELF'], '/~') === false) {
            $root_path = substr($uri, 0, strpos($uri, '/', 1)) . $root_path;
        }

        // set hostname
        $address = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '::1';

        // check for localhost variations
        if ($name == 'localhost' || $address == '::1' || $address == '127.0.0.1') {
            $this->host = 'localhost';
        } else {
            $this->host = $name;
        }

        $this->base = $base;
        $this->root = $base . $root_path;
        $this->url = $base . $uri;
    }

    /**
     * Initializes the URI object based on the url set on the object
     */
    public function init()
    {
        $grav = Grav::instance();

        $config = $grav['config'];
        $language = $grav['language'];

        // resets
        $this->paths = [];
        $this->params = [];
        $this->query = [];

        // get any params and remove them
        $uri = str_replace($this->root, '', $this->url);

        // remove double slashes
        $uri = preg_replace('#/{2,}#', '/', $uri);

        // remove the setup.php based base if set:
        $setup_base = $grav['pages']->base();
        if ($setup_base) {
            $uri = str_replace($setup_base, '', $uri);
        }

        // If configured to, redirect trailing slash URI's with a 301 redirect
        if ($config->get('system.pages.redirect_trailing_slash', false) && $uri != '/' && Utils::endsWith($uri, '/')) {
            $grav->redirect(rtrim($uri, '/'), 301);
        }

        // process params
        $uri = $this->processParams($uri, $config->get('system.param_sep'));

        // set active language
        $uri = $language->setActiveFromUri($uri);

        // split the URL and params
        $bits = parse_url($uri);

        // process query string
        if (isset($bits['query']) && isset($bits['path'])) {
            $this->query = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
            $uri = $bits['path'];
        }

        // remove the extension if there is one set
        $parts = pathinfo($uri);

        // set the original basename
        $this->basename = $parts['basename'];

        // set the extension
        if (isset($parts['extension'])) {
            $this->extension = $parts['extension'];
        }

        $valid_page_types = implode('|', $config->get('system.pages.types'));

        if (preg_match("/\.(".$valid_page_types.")$/", $parts['basename'])) {
            $uri = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, $parts['dirname']), DS). '/' .$parts['filename'];
        }

        // set the new url
        $this->url = $this->root . $uri;
        $this->path = $uri;
        $this->content_path = trim(str_replace($this->base, '', $this->path), '/');
        if ($this->content_path != '') {
            $this->paths = explode('/', $this->content_path);
        }
    }

    /**
     * Process any params based in this URL, supports any valid delimiter
     *
     * @param        $uri
     * @param string $delimiter
     *
     * @return string
     */
    private function processParams($uri, $delimiter = ':')
    {
        if (strpos($uri, $delimiter) !== false) {
            $bits = explode('/', $uri);
            $path = array();
            foreach ($bits as $bit) {
                if (strpos($bit, $delimiter) !== false) {
                    $param = explode($delimiter, $bit);
                    if (count($param) == 2) {
                        $plain_var = filter_var(urldecode($param[1]), FILTER_SANITIZE_STRING);
                        $this->params[$param[0]] = $plain_var;
                    }
                } else {
                    $path[] = $bit;
                }
            }
            $uri = '/' . ltrim(implode('/', $path), '/');
        }
        return $uri;
    }

    /**
     * Return URI path.
     *
     * @param  string  $id
     * @return string
     */
    public function paths($id = null)
    {
        if (isset($id)) {
            return $this->paths[$id];
        } else {
            return $this->paths;
        }
    }

    /**
     * Return route to the current URI. By default route doesn't include base path.
     *
     * @param  bool  $absolute  True to include full path.
     * @param  bool  $domain    True to include domain. Works only if first parameter is also true.
     * @return string
     */
    public function route($absolute = false, $domain = false)
    {
        return urldecode(($absolute ? $this->rootUrl($domain) : '') . '/' . implode('/', $this->paths));
    }

    /**
     * Return full query string or a single query attribute.
     *
     * @param  string  $id  Optional attribute.
     * @return string
     */
    public function query($id = null, $raw = false)
    {
        if (isset($id)) {
            return isset($this->query[$id]) ? $this->query[$id] : null;
        } else {
            if ($raw) {
                return $this->query;
            } else {
                return http_build_query($this->query);
            }
        }
    }

    /**
     * Return all or a single query parameter as a URI compatible string.
     *
     * @param  string  $id  Optional parameter name.
     * @param  boolean $array return the array format or not
     * @return null|string
     */
    public function params($id = null, $array = false)
    {
        $config = Grav::instance()['config'];

        $params = null;
        if ($id === null) {
            if ($array) {
                return $this->params;
            }
            $output = array();
            foreach ($this->params as $key => $value) {
                $output[] = $key . $config->get('system.param_sep') . $value;
                $params = '/'.implode('/', $output);
            }
        } elseif (isset($this->params[$id])) {
            if ($array) {
                return $this->params[$id];
            }
            $params = "/{$id}". $config->get('system.param_sep') . $this->params[$id];
        }

        return $params;
    }

    /**
     * Get URI parameter.
     *
     * @param  string  $id
     * @return bool|string
     */
    public function param($id)
    {
        if (isset($this->params[$id])) {
            return urldecode($this->params[$id]);
        } else {
            return false;
        }
    }

    /**
     * Return URL.
     *
     * @param  bool  $include_host  Include hostname.
     * @return string
     */
    public function url($include_host = false)
    {
        if ($include_host) {
            return $this->url;
        } else {
            $url = (str_replace($this->base, '', rtrim($this->url, '/')));
            return $url ? $url : '/';
        }
    }

    /**
     * Return the Path
     *
     * @return String The path of the URI
     */
    public function path()
    {
        $path = $this->path;
        if ($path === '') {
            $path = '/';
        }
        return $path;
    }

    /**
     * Return the Extension of the URI
     *
     * @param null $default
     *
     * @return String The extension of the URI
     */
    public function extension($default = null)
    {
        if (!$this->extension) {
            $this->extension = $default;
        }
        return $this->extension;
    }

    /**
     * Return the host of the URI
     *
     * @return String The host of the URI
     */
    public function host()
    {
        return $this->host;
    }

    /**
     * Gets the environment name
     *
     * @return String
     */
    public function environment()
    {
        return $this->host();
    }


    /**
     * Return the basename of the URI
     *
     * @return String The basename of the URI
     */
    public function basename()
    {
        return $this->basename;
    }

    /**
     * Return the base of the URI
     *
     * @return String The base of the URI
     */
    public function base()
    {
        return $this->base;
    }

    /**
     * Return root URL to the site.
     *
     * @param  bool  $include_host Include hostname.
     * @return mixed
     */
    public function rootUrl($include_host = false)
    {
        if ($include_host) {
            return $this->root;
        } else {
            $root = str_replace($this->base, '', $this->root);
            return $root;
        }
    }

    /**
     * Return current page number.
     *
     * @return int
     */
    public function currentPage()
    {
        if (isset($this->params['page'])) {
            return $this->params['page'];
        } else {
            return 1;
        }
    }

    /**
     * Return relative path to the referrer defaulting to current or given page.
     *
     * @param string $default
     * @param string $attributes
     * @return string
     */
    public function referrer($default = null, $attributes = null)
    {
        $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;

        // Check that referrer came from our site.
        $root = $this->rootUrl(true);
        if ($referrer) {
            // Referrer should always have host set and it should come from the same base address.
            if (stripos($referrer, $root) !== 0) {
                $referrer = null;
            }
        }

        if (!$referrer) {
            $referrer = $default ? $default : $this->route(true, true);
        }

        if ($attributes) {
            $referrer .= $attributes;
        }

        // Return relative path.
        return substr($referrer, strlen($root));
    }

    /**
     * Return the IP address of the current user
     *
     * @return string ip address
     */
    public function ip()
    {
        if (getenv('HTTP_CLIENT_IP'))
            $ipaddress = getenv('HTTP_CLIENT_IP');
        else if(getenv('HTTP_X_FORWARDED_FOR'))
            $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
        else if(getenv('HTTP_X_FORWARDED'))
            $ipaddress = getenv('HTTP_X_FORWARDED');
        else if(getenv('HTTP_FORWARDED_FOR'))
            $ipaddress = getenv('HTTP_FORWARDED_FOR');
        else if(getenv('HTTP_FORWARDED'))
           $ipaddress = getenv('HTTP_FORWARDED');
        else if(getenv('REMOTE_ADDR'))
            $ipaddress = getenv('REMOTE_ADDR');
        else
            $ipaddress = 'UNKNOWN';
        return $ipaddress;

    }

    /**
     * Is this an external URL? if it starts with `http` then yes, else false
     *
     * @param  string  $url the URL in question
     * @return boolean      is eternal state
     */
    public function isExternal($url)
    {
        if (Utils::startsWith($url, 'http')) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * The opposite of built-in PHP method parse_url()
     *
     * @param $parsed_url
     * @return string
     */
    public static function buildUrl($parsed_url)
    {
        $scheme   = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
        $host     = isset($parsed_url['host']) ? $parsed_url['host'] : '';
        $port     = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
        $user     = isset($parsed_url['user']) ? $parsed_url['user'] : '';
        $pass     = isset($parsed_url['pass']) ? ':' . $parsed_url['pass']  : '';
        $pass     = ($user || $pass) ? "$pass@" : '';
        $path     = isset($parsed_url['path']) ? $parsed_url['path'] : '';
        $query    = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
        $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
        return "$scheme$user$pass$host$port$path$query$fragment";
    }

    /**
     * Converts links from absolute '/' or relative (../..) to a grav friendly format
     *
     * @param         $page         the current page to use as reference
     * @param  string $markdown_url the URL as it was written in the markdown
     *
     * @return string the more friendly formatted url
     */
    public static function convertUrl(Page $page, $markdown_url, $type = 'link')
    {
        $grav = Grav::instance();

        /** @var Grav\Common\Language\Language $language */
        $language = $grav['language'];

        // Link processing should prepend language
        $language_append = '';
        if ($type == 'link' && $language->enabled()) {
            $language_append = $language->getLanguageURLPrefix();
        }

        $pages_dir = $grav['locator']->findResource('page://');
        $base_url = rtrim($grav['base_url'] . $grav['pages']->base(), '/') . $language_append;

        // if absolute and starts with a base_url move on
        if (pathinfo($markdown_url, PATHINFO_DIRNAME) == '.' && $page->url() == '/') {
            return '/' . $markdown_url;
            // no path to convert
        } elseif ($base_url != '' && Utils::startsWith($markdown_url, $base_url)) {
            return $markdown_url;
            // if contains only a fragment
        } elseif (Utils::startsWith($markdown_url, '#')) {
            return $markdown_url;
        } else {
            $target = null;
            // see if page is relative to this or absolute
            if (Utils::startsWith($markdown_url, '/')) {
                $normalized_url = Utils::normalizePath($base_url . $markdown_url);
                $normalized_path = Utils::normalizePath($pages_dir . $markdown_url);
            } else {
                $normalized_url = $base_url . Utils::normalizePath($page->route() . '/' . $markdown_url);
                $normalized_path = Utils::normalizePath($page->path() . '/' . $markdown_url);
            }

            // special check to see if path checking is required.
            $just_path = str_replace($normalized_url, '', $normalized_path);
            if ($just_path == $page->path()) {
                return $normalized_url;
            }

            $url_bits = parse_url($normalized_path);
            $full_path = ($url_bits['path']);

            if (file_exists($full_path)) {
                // do nothing
            } elseif (file_exists(urldecode($full_path))) {
                $full_path = urldecode($full_path);
            } else {
                return $normalized_url;
            }

            $path_info = pathinfo($full_path);
            $page_path = $path_info['dirname'];
            $filename = '';


            if ($markdown_url == '..') {
                $page_path = $full_path;
            } else {
                // save the filename if a file is part of the path
                if (is_file($full_path)) {
                    if ($path_info['extension'] != 'md') {
                        $filename = '/' . $path_info['basename'];
                    }
                } else {
                    $page_path = $full_path;
                }
            }

            // get page instances and try to find one that fits
            $instances = $grav['pages']->instances();
            if (isset($instances[$page_path])) {
                $target = $instances[$page_path];
                $url_bits['path'] = $base_url . rtrim($target->route(), '/') . $filename;
                return Uri::buildUrl($url_bits);
            }

            return $normalized_url;
        }
    }
}
