Newer
Older
framework / system / Helpers / url_helper.php
@MGatner MGatner on 18 May 2021 17 KB Release v4.1.2
<?php

/**
 * This file is part of the CodeIgniter 4 framework.
 *
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Router\Exceptions\RouterException;
use Config\App;
use Config\Services;

/**
 * CodeIgniter URL Helpers
 */

if (! function_exists('_get_uri'))
{
	/**
	 * Used by the other URL functions to build a
	 * framework-specific URI based on the App config.
	 *
	 * @internal Outside of the framework this should not be used directly.
	 *
	 * @param string   $relativePath May include queries or fragments
	 * @param App|null $config
	 *
	 * @return URI
	 *
	 * @throws InvalidArgumentException For invalid paths or config
	 */
	function _get_uri(string $relativePath = '', App $config = null): URI
	{
		$config = $config ?? config('App');

		if ($config->baseURL === '')
		{
			throw new InvalidArgumentException('_get_uri() requires a valid baseURL.');
		}

		// If a full URI was passed then convert it
		if (is_int(strpos($relativePath, '://')))
		{
			$full         = new URI($relativePath);
			$relativePath = URI::createURIString(null, null, $full->getPath(), $full->getQuery(), $full->getFragment());
		}

		$relativePath = URI::removeDotSegments($relativePath);

		// Build the full URL based on $config and $relativePath
		$url = rtrim($config->baseURL, '/ ') . '/';

		// Check for an index page
		if ($config->indexPage !== '')
		{
			$url .= $config->indexPage;

			// Check if we need a separator
			if ($relativePath !== '' && $relativePath[0] !== '/' && $relativePath[0] !== '?')
			{
				$url .= '/';
			}
		}

		$url .= $relativePath;

		$uri = new URI($url);

		// Check if the baseURL scheme needs to be coerced into its secure version
		if ($config->forceGlobalSecureRequests && $uri->getScheme() === 'http')
		{
			$uri->setScheme('https');
		}

		return $uri;
	}
}

//--------------------------------------------------------------------

if (! function_exists('site_url'))
{
	/**
	 * Returns a site URL as defined by the App config.
	 *
	 * @param mixed       $relativePath URI string or array of URI segments
	 * @param string|null $scheme
	 * @param App|null    $config       Alternate configuration to use
	 *
	 * @return string
	 */
	function site_url($relativePath = '', string $scheme = null, App $config = null): string
	{
		// Convert array of segments to a string
		if (is_array($relativePath))
		{
			$relativePath = implode('/', $relativePath);
		}

		$uri = _get_uri($relativePath, $config);

		return URI::createURIString($scheme ?? $uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery(), $uri->getFragment());
	}
}

//--------------------------------------------------------------------

if (! function_exists('base_url'))
{
	/**
	 * Returns the base URL as defined by the App config.
	 * Base URLs are trimmed site URLs without the index page.
	 *
	 * @param  mixed  $relativePath URI string or array of URI segments
	 * @param  string $scheme
	 * @return string
	 */
	function base_url($relativePath = '', string $scheme = null): string
	{
		$config            = clone config('App');
		$config->indexPage = '';

		return rtrim(site_url($relativePath, $scheme, $config), '/');
	}
}

//--------------------------------------------------------------------

if (! function_exists('current_url'))
{
	/**
	 * Returns the current full URL based on the IncomingRequest.
	 * String returns ignore query and fragment parts.
	 *
	 * @param boolean              $returnObject True to return an object instead of a string
	 * @param IncomingRequest|null $request      A request to use when retrieving the path
	 *
	 * @return string|URI
	 */
	function current_url(bool $returnObject = false, IncomingRequest $request = null)
	{
		$request = $request ?? Services::request();
		$path    = $request->getPath();

		// Append queries and fragments
		if ($query = $request->getUri()->getQuery())
		{
			$path .= '?' . $query;
		}
		if ($fragment = $request->getUri()->getFragment())
		{
			$path .= '#' . $fragment;
		}

		$uri = _get_uri($path);

		return $returnObject ? $uri : URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath());
	}
}

//--------------------------------------------------------------------

if (! function_exists('previous_url'))
{
	/**
	 * Returns the previous URL the current visitor was on. For security reasons
	 * we first check in a saved session variable, if it exists, and use that.
	 * If that's not available, however, we'll use a sanitized url from $_SERVER['HTTP_REFERER']
	 * which can be set by the user so is untrusted and not set by certain browsers/servers.
	 *
	 * @param boolean $returnObject
	 *
	 * @return URI|mixed|string
	 */
	function previous_url(bool $returnObject = false)
	{
		// Grab from the session first, if we have it,
		// since it's more reliable and safer.
		// Otherwise, grab a sanitized version from $_SERVER.
		$referer = $_SESSION['_ci_previous_url'] ?? Services::request()->getServer('HTTP_REFERER', FILTER_SANITIZE_URL);

		$referer = $referer ?? site_url('/');

		return $returnObject ? new URI($referer) : $referer;
	}
}

//--------------------------------------------------------------------

if (! function_exists('uri_string'))
{
	/**
	 * URL String
	 *
	 * Returns the path part of the current URL
	 *
	 * @param boolean $relative Whether the resulting path should be relative to baseURL
	 *
	 * @return string
	 */
	function uri_string(bool $relative = false): string
	{
		$request = Services::request();
		$uri     = $request->uri;

		// An absolute path is equivalent to getPath()
		if (! $relative)
		{
			return $uri->getPath();
		}

		// Remove the baseURL from the entire URL
		$url     = (string) $uri->__toString();
		$baseURL = rtrim($request->config->baseURL, '/ ') . '/';

		return substr($url, strlen($baseURL));
	}
}

//--------------------------------------------------------------------

if (! function_exists('index_page'))
{
	/**
	 * Index page
	 *
	 * Returns the "index_page" from your config file
	 *
	 * @param  App|null $altConfig Alternate configuration to use
	 * @return string
	 */
	function index_page(App $altConfig = null): string
	{
		// use alternate config if provided, else default one
		$config = $altConfig ?? config(App::class);

		return $config->indexPage;
	}
}

// ------------------------------------------------------------------------

if (! function_exists('anchor'))
{
	/**
	 * Anchor Link
	 *
	 * Creates an anchor based on the local URL.
	 *
	 * @param mixed    $uri        URI string or array of URI segments
	 * @param string   $title      The link title
	 * @param mixed    $attributes Any attributes
	 * @param App|null $altConfig  Alternate configuration to use
	 *
	 * @return string
	 */
	function anchor($uri = '', string $title = '', $attributes = '', App $altConfig = null): string
	{
		// use alternate config if provided, else default one
		$config = $altConfig ?? config(App::class);

		$siteUrl = is_array($uri) ? site_url($uri, null, $config) : (preg_match('#^(\w+:)?//#i', $uri) ? $uri : site_url($uri, null, $config));
		// eliminate trailing slash
		$siteUrl = rtrim($siteUrl, '/');

		if ($title === '')
		{
			$title = $siteUrl;
		}

		if ($attributes !== '')
		{
			$attributes = stringify_attributes($attributes);
		}

		return '<a href="' . $siteUrl . '"' . $attributes . '>' . $title . '</a>';
	}
}

// ------------------------------------------------------------------------

if (! function_exists('anchor_popup'))
{
	/**
	 * Anchor Link - Pop-up version
	 *
	 * Creates an anchor based on the local URL. The link
	 * opens a new window based on the attributes specified.
	 *
	 * @param string   $uri        the URL
	 * @param string   $title      the link title
	 * @param mixed    $attributes any attributes
	 * @param App|null $altConfig  Alternate configuration to use
	 *
	 * @return string
	 */
	function anchor_popup($uri = '', string $title = '', $attributes = false, App $altConfig = null): string
	{
		// use alternate config if provided, else default one
		$config = $altConfig ?? config(App::class);

		$siteUrl = preg_match('#^(\w+:)?//#i', $uri) ? $uri : site_url($uri, null, $config);
		$siteUrl = rtrim($siteUrl, '/');

		if ($title === '')
		{
			$title = $siteUrl;
		}

		if ($attributes === false)
		{
			return '<a href="' . $siteUrl . '" onclick="window.open(\'' . $siteUrl . "', '_blank'); return false;\">" . $title . '</a>';
		}

		if (! is_array($attributes))
		{
			$attributes = [$attributes];

			// Ref: http://www.w3schools.com/jsref/met_win_open.asp
			$windowName = '_blank';
		}
		elseif (! empty($attributes['window_name']))
		{
			$windowName = $attributes['window_name'];
			unset($attributes['window_name']);
		}
		else
		{
			$windowName = '_blank';
		}

		foreach (['width' => '800', 'height' => '600', 'scrollbars' => 'yes', 'menubar' => 'no', 'status' => 'yes', 'resizable' => 'yes', 'screenx' => '0', 'screeny' => '0'] as $key => $val)
		{
			$atts[$key] = $attributes[$key] ?? $val;
			unset($attributes[$key]);
		}

		$attributes = stringify_attributes($attributes);

		return '<a href="' . $siteUrl
				. '" onclick="window.open(\'' . $siteUrl . "', '" . $windowName . "', '" . stringify_attributes($atts, true) . "'); return false;\""
				. $attributes . '>' . $title . '</a>';
	}
}

// ------------------------------------------------------------------------

if (! function_exists('mailto'))
{
	/**
	 * Mailto Link
	 *
	 * @param string $email      the email address
	 * @param string $title      the link title
	 * @param mixed  $attributes any attributes
	 *
	 * @return string
	 */
	function mailto(string $email, string $title = '', $attributes = ''): string
	{
		if (trim($title) === '')
		{
			$title = $email;
		}

		return '<a href="mailto:' . $email . '"' . stringify_attributes($attributes) . '>' . $title . '</a>';
	}
}

// ------------------------------------------------------------------------

if (! function_exists('safe_mailto'))
{
	/**
	 * Encoded Mailto Link
	 *
	 * Create a spam-protected mailto link written in Javascript
	 *
	 * @param string $email      the email address
	 * @param string $title      the link title
	 * @param mixed  $attributes any attributes
	 *
	 * @return string
	 */
	function safe_mailto(string $email, string $title = '', $attributes = ''): string
	{
		if (trim($title) === '')
		{
			$title = $email;
		}

		$x = str_split('<a href="mailto:', 1);

		for ($i = 0, $l = strlen($email); $i < $l; $i ++)
		{
			$x[] = '|' . ord($email[$i]);
		}

		$x[] = '"';

		if ($attributes !== '')
		{
			if (is_array($attributes))
			{
				foreach ($attributes as $key => $val)
				{
					$x[] = ' ' . $key . '="';
					for ($i = 0, $l = strlen($val); $i < $l; $i ++)
					{
						$x[] = '|' . ord($val[$i]);
					}
					$x[] = '"';
				}
			}
			else
			{
				for ($i = 0, $l = mb_strlen($attributes); $i < $l; $i ++)
				{
					$x[] = mb_substr($attributes, $i, 1);
				}
			}
		}

		$x[] = '>';

		$temp = [];
		for ($i = 0, $l = strlen($title); $i < $l; $i ++)
		{
			$ordinal = ord($title[$i]);

			if ($ordinal < 128)
			{
				$x[] = '|' . $ordinal;
			}
			else
			{
				if (empty($temp))
				{
					$count = ($ordinal < 224) ? 2 : 3;
				}

				$temp[] = $ordinal;
				if (count($temp) === $count) // @phpstan-ignore-line
				{
					$number = ($count === 3) ? (($temp[0] % 16) * 4096) + (($temp[1] % 64) * 64) + ($temp[2] % 64) : (($temp[0] % 32) * 64) + ($temp[1] % 64);
					$x[]    = '|' . $number;
					$count  = 1;
					$temp   = [];
				}
			}
		}

		$x[] = '<';
		$x[] = '/';
		$x[] = 'a';
		$x[] = '>';

		$x = array_reverse($x);

		// improve obfuscation by eliminating newlines & whitespace
		$output = '<script type="text/javascript">'
				. 'var l=new Array();';

		foreach ($x as $i => $value)
		{
			$output .= 'l[' . $i . "] = '" . $value . "';";
		}

		return $output . ('for (var i = l.length-1; i >= 0; i=i-1) {'
				. "if (l[i].substring(0, 1) === '|') document.write(\"&#\"+unescape(l[i].substring(1))+\";\");"
				. 'else document.write(unescape(l[i]));'
				. '}'
				. '</script>');
	}
}

// ------------------------------------------------------------------------

if (! function_exists('auto_link'))
{
	/**
	 * Auto-linker
	 *
	 * Automatically links URL and Email addresses.
	 * Note: There's a bit of extra code here to deal with
	 * URLs or emails that end in a period. We'll strip these
	 * off and add them after the link.
	 *
	 * @param string  $str   the string
	 * @param string  $type  the type: email, url, or both
	 * @param boolean $popup whether to create pop-up links
	 *
	 * @return string
	 */
	function auto_link(string $str, string $type = 'both', bool $popup = false): string
	{
		// Find and replace any URLs.
		if ($type !== 'email' && preg_match_all('#(\w*://|www\.)[^\s()<>;]+\w#i', $str, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER))
		{
			// Set our target HTML if using popup links.
			$target = ($popup) ? ' target="_blank"' : '';

			// We process the links in reverse order (last -> first) so that
			// the returned string offsets from preg_match_all() are not
			// moved as we add more HTML.
			foreach (array_reverse($matches) as $match)
			{
				// $match[0] is the matched string/link
				// $match[1] is either a protocol prefix or 'www.'
				//
				// With PREG_OFFSET_CAPTURE, both of the above is an array,
				// where the actual value is held in [0] and its offset at the [1] index.
				$a   = '<a href="' . (strpos($match[1][0], '/') ? '' : 'http://') . $match[0][0] . '"' . $target . '>' . $match[0][0] . '</a>';
				$str = substr_replace($str, $a, $match[0][1], strlen($match[0][0]));
			}
		}

		// Find and replace any emails.
		if ($type !== 'url' && preg_match_all('#([\w\.\-\+]+@[a-z0-9\-]+\.[a-z0-9\-\.]+[^[:punct:]\s])#i', $str, $matches, PREG_OFFSET_CAPTURE))
		{
			foreach (array_reverse($matches[0]) as $match)
			{
				if (filter_var($match[0], FILTER_VALIDATE_EMAIL) !== false)
				{
					$str = substr_replace($str, safe_mailto($match[0]), $match[1], strlen($match[0]));
				}
			}
		}

		return $str;
	}
}

// ------------------------------------------------------------------------

if (! function_exists('prep_url'))
{
	/**
	 * Prep URL - Simply adds the http:// or https:// part if no scheme is included.
	 *
	 * Formerly used URI, but that does not play nicely with URIs missing
	 * the scheme.
	 *
	 * @param  string  $str    the URL
	 * @param  boolean $secure set true if you want to force https://
	 * @return string
	 */
	function prep_url(string $str = '', bool $secure = false): string
	{
		if (in_array($str, ['http://', 'https://', '//', ''], true))
		{
			return '';
		}

		if (parse_url($str, PHP_URL_SCHEME) === null)
		{
			$str = 'http://' . ltrim($str, '/');
		}

		// force replace http:// with https://
		if ($secure)
		{
			$str = preg_replace('/^(?:http):/i', 'https:', $str);
		}

		return $str;
	}
}

// ------------------------------------------------------------------------

if (! function_exists('url_title'))
{
	/**
	 * Create URL Title
	 *
	 * Takes a "title" string as input and creates a
	 * human-friendly URL string with a "separator" string
	 * as the word separator.
	 *
	 * @param  string  $str       Input string
	 * @param  string  $separator Word separator (usually '-' or '_')
	 * @param  boolean $lowercase Whether to transform the output string to lowercase
	 * @return string
	 */
	function url_title(string $str, string $separator = '-', bool $lowercase = false): string
	{
		$qSeparator = preg_quote($separator, '#');

		$trans = [
			'&.+?;'                  => '',
			'[^\w\d\pL\pM _-]'       => '',
			'\s+'                    => $separator,
			'(' . $qSeparator . ')+' => $separator,
		];

		$str = strip_tags($str);
		foreach ($trans as $key => $val)
		{
			$str = preg_replace('#' . $key . '#iu', $val, $str);
		}

		if ($lowercase === true)
		{
			$str = mb_strtolower($str);
		}

		return trim(trim($str, $separator));
	}
}

// ------------------------------------------------------------------------

if (! function_exists('mb_url_title'))
{
	/**
	 * Create URL Title that takes into account accented characters
	 *
	 * Takes a "title" string as input and creates a
	 * human-friendly URL string with a "separator" string
	 * as the word separator.
	 *
	 * @param  string  $str       Input string
	 * @param  string  $separator Word separator (usually '-' or '_')
	 * @param  boolean $lowercase Whether to transform the output string to lowercase
	 * @return string
	 */
	function mb_url_title(string $str, string $separator = '-', bool $lowercase = false): string
	{
		helper('text');

		return url_title(convert_accented_characters($str), $separator, $lowercase);
	}
}

//--------------------------------------------------------------------

if (! function_exists('url_to'))
{
	/**
	 * Get the full, absolute URL to a controller method
	 * (with additional arguments)
	 *
	 * @param string $controller
	 * @param mixed  ...$args
	 *
	 * @throws RouterException
	 *
	 * @return string
	 */
	function url_to(string $controller, ...$args): string
	{
		if (! $route = route_to($controller, ...$args))
		{
			$explode = explode('::', $controller);

			if (isset($explode[1]))
			{
				throw RouterException::forControllerNotFound($explode[0], $explode[1]);
			}

			throw RouterException::forInvalidRoute($controller);
		}

		return site_url($route);
	}
}

if (! function_exists('url_is'))
{
	/**
	 * Determines if current url path contains
	 * the given path. It may contain a wildcard (*)
	 * which will allow any valid character.
	 *
	 * Example:
	 *   if (url_is('admin*)) ...
	 *
	 * @param string $path
	 *
	 * @return boolean
	 */
	function url_is(string $path): bool
	{
		// Setup our regex to allow wildcards
		$path        = '/' . trim(str_replace('*', '(\S)*', $path), '/ ');
		$currentPath = '/' . trim(uri_string(true), '/ ');

		return (bool) preg_match("|^{$path}$|", $currentPath, $matches);
	}
}