<?php
/**
 * @brief		Internal URL
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @since		11 May 2015
 */

namespace IPS\Http\Url;
 
/* To prevent PHP errors (extending class does not exist) revealing path */

use Exception;
use IPS\Content;
use IPS\Http\Url;
use IPS\Member\Club;
use IPS\Node\Model;
use IPS\cms\Pages\Router;
use IPS\Session;
use IPS\Application;
use OutOfRangeException;
use function count;
use function defined;
use function intval;
use function is_numeric;
use function is_string;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Internal URL
 */
class Internal extends Url
{
	/**
	 * Create internal URL from components - all arguments should be UNENCODED
	 *
	 * @param string $base		Key for the URL base
	 * @param int|string|null $protocol	Protocol (one of the PROTOCOL_* constants) or an actual scheme
	 * @param string $path		Path, relative to site's base URL
	 * @param array|string|null $query		Query
	 * @param string|null $fragment	Fragment
	 * @return	Url
	 */
	protected static function createInternalFromComponents( string $base, int|string|null $protocol, string $path, array|string $query = NULL, string $fragment = NULL ): Url
	{		
		$baseUrlData = parse_url( static::baseUrl( is_numeric( $protocol ) ? $protocol : NULL ) );
		
		$pathToUse = '';
		if ( isset( $baseUrlData['path'] ) and $baseUrlData['path'] )
		{
			$pathToUse .= trim( $baseUrlData['path'], '/' );
		}
		if ( $path or $query or $fragment or $pathToUse )
		{
			if ( $pathToUse )
			{
				$pathToUse .= '/';
			}
			$pathToUse .= ltrim( $path, '/' );
		}
								
		$return = static::createFromComponents(
			$baseUrlData['host'],
			( is_string( $protocol ) and !is_numeric( $protocol ) ) ? $protocol : ( ( isset( $baseUrlData['scheme'] ) AND $protocol !== NULL ) ? $baseUrlData['scheme'] : NULL ),
			$pathToUse ? "/{$pathToUse}" : '',
			$query,
			$baseUrlData['port'] ?? NULL,
			$baseUrlData['user'] ?? NULL,
			$baseUrlData['pass'] ?? NULL,
			$fragment
		);
		
		$return->base = $base;
		return $return;
	}
	
	/**
	 * @brief	Does this URL have the SEO pagination (page/n/)?
	 */
	public ?bool $seoPagination = NULL;
	
	/**
	 * @brief	Base
	 */
	public ?string $base = NULL;
	
	/**
	 * Get the friendly URL for this URL if there is one
	 *
	 * @return	mixed	The friendly URL if there is one, TRUE if there isn't, or NULL if not sure
	 */
	public function correctFriendlyUrl(): mixed
	{
		$return = FALSE;

		foreach (Friendly::furlDefinition() as $seoTemplate => $furlDefinition )
		{
			if( mb_stripos( $this->data[ static::COMPONENT_QUERY ], $furlDefinition['real'] ) !== FALSE )
			{
				/* Figure out if this FURL definition requires extra data.
					Example: messenger_convo and messenger have the same $furlDefinition['real'] definition, but messenger_convo requires an 'id' parameter too */
				$params	= array();
				preg_match_all( '/{(.+?)}/', $furlDefinition['friendly'], $matches );
				foreach ( $matches[1] as $tag )
				{
					switch ( mb_substr( $tag, 0, 1 ) )
					{
						case '#':
						case '@':
							$params[] = mb_substr( $tag, 1 );
							break;
					}
				}

				/* If this definition requires a parameter, see if we have it.  If not, skip to next definition to check. */
				if( count( array_diff( $params, array_keys( $this->queryString ) ) ) )
				{
					continue;
				}

				/* Now try to check URL */
				try
				{
					/* If there's a "verify" method, we can use that to get the correct URL */
					if( !empty( $furlDefinition['verify'] ) )
					{
						/* Load it */
						try
						{
							if ( ! $return = $this->correctUrlFromVerifyClass( $furlDefinition['verify'] ) )
							{
								$return = NULL;
							}
						}
						/* It doesn't exist */
						catch ( OutOfRangeException $e )
						{
							$return = NULL;
						}
					}
					/* If not, we have to build it ourself */
					else
					{
						$seoTitles = array();
						if ( isset( $furlDefinition['seoTitles'] ) )
						{
							foreach ( $furlDefinition['seoTitles'] as $seoTitleData )
							{
								try
								{
									$class = $seoTitleData['class'];
									$queryParam = $seoTitleData['queryParam'];
									$property = $seoTitleData['property'];
																						
									$seoTitles[] = $class::load( $this->queryString[ $queryParam ] )->$property;
								}
								catch ( OutOfRangeException $e ) {}
							}
						}
						
						$return = Url::internal( $this->data['query'], 'front', $seoTemplate, $seoTitles );
					}
				}
				catch ( Exception $e ) {}
			}
		}

		/* If it it thinks it belongs to "pages", we might be able to be more accurate */
		if ( Application::appIsEnabled( 'cms' ) and ( $return instanceof Friendly and $return->seoTemplate === 'content_page_path' and isset( $this->queryString['path'] ) ) )
		{
			/* Try to find a page */
			try
			{
				/* Create it */
				$correctUrl = Router::loadFromUrl( $this )->url();

				/* Set extra stuff in our query string */
				$paramsToSet = array();
				foreach ( $this->queryString as $k => $v )
				{
					if ( !array_key_exists( $k, $correctUrl->queryString ) and !array_key_exists( $k, $correctUrl->hiddenQueryString ) )
					{
						$paramsToSet[ $k ] = $v;
					}
				}
				if ( count( $paramsToSet ) )
				{
					$correctUrl = $correctUrl->setQueryString( $paramsToSet );
				}

				/* Return */
				$return = $correctUrl;
			}
				/* Couldn't find one? Don't accept responsibility */
			catch (OutOfRangeException $e ){}
		}
		else if( $return !== NULL and !( $return instanceof Friendly ) )
		{
			$return = TRUE;
		}

		return $return;
	}
	
	/**
	 * Get the correct URL using a "verify" property from the FURL definition
	 *
	 * @param mixed $verify	Class containing `preCorrectUrlFromVerifyClass()` method to check URL against
	 * @return	NULL|Url
	 * @throws	OutOfRangeException
	 */
	protected function correctUrlFromVerifyClass( mixed $verify ): ?Url
	{
		/* Load it */
		if ( method_exists( $verify, 'preCorrectUrlFromVerifyClass' ) )
		{
			$verify::preCorrectUrlFromVerifyClass( $this );
		}
		$contentObject = $verify::loadFromUrl( $this );
		
		/* Check the currently logged in user can view it, as we don't want to reveal the correct
			URL to something the user has no permission to view */
		$canView = TRUE;
		if ( $contentObject instanceof Content or $contentObject instanceof Club )
		{
			$canView = $contentObject->canView();
		}
		elseif ( $contentObject instanceof Model )
		{
			$canView = $contentObject->can('view');
		}
		if ( !$canView )
		{
			return NULL;
		} 
		
		/* Create it */
		$correctUrl = $contentObject->url();
		
		/* Set extra stuff in our query string */
		$paramsToSet = array();
		foreach ( $this->queryString as $k => $v )
		{
			if ( !array_key_exists( $k, $correctUrl->queryString ) and !array_key_exists( $k, $correctUrl->hiddenQueryString ) )
			{
				$paramsToSet[ $k ] = $v;
			}
		}
		if ( count( $paramsToSet ) )
		{
			$correctUrl = $correctUrl->setQueryString( $paramsToSet );
		}
		
		/* Return */
		return $correctUrl;
	}
	
	/**
	 * Add CSRF check to query string
	 *
	 * @return	Url
	 */
	public function csrf(): Url
	{
		return $this->setQueryString( 'csrfKey', Session::i()->csrfKey );
	}
	
	/**
	 * Adds the page parameter to the URL
	 *
	 * @param string $param	The page key, default is 'page'
	 * @param int|null $number	The page number to use
	 * @return	Url
	 */
	public function setPage( string $param='page', ?int $number=1 ): Url
	{
		/* Remove any existing page param */
		$url = $this->stripQueryString( $param );
		
		if ( intval( $number ) > 1 )
		{
			$url = $this->setQueryString( $param, $number );
		}
		
		return $url;
	}
	
	/**
	 * Get ACP query string without adsess
	 *
	 * @return	string
	 */
	public function acpQueryString(): string
	{
		$queryString = $this->queryString;
		unset( $queryString['csrf'] );
		return static::convertQueryAsArrayToString( $queryString );
	}
	
	/**
	 * Check if the URL is an open redirect.
	 * Useful if you are checking if a given URL is internal to ensure it also doesn't redirect elsewhere
	 *
	 * @return	bool
	 */
	public function openRedirect(): bool
	{
		return isset( $this->queryString['app'] ) and $this->queryString['app'] === 'core' and isset( $this->queryString['module'] ) and $this->queryString['module'] === 'system' and isset( $this->queryString['controller'] ) and $this->queryString['controller'] === 'redirect';
	}
}