Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+7 -3
View File
@@ -11,16 +11,20 @@
namespace IPS\Http\Url;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* URL Exception
*/
class _Exception extends \DomainException
class Exception extends DomainException
{
}
+240 -113
View File
@@ -11,51 +11,69 @@
namespace IPS\Http\Url;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Application;
use IPS\cms\Pages\Page;
use IPS\Data\Store;
use IPS\Http\Url;
use IPS\Settings;
use IPS\Db;
use OutOfRangeException;
use UnderflowException;
use function count;
use function defined;
use function file_get_contents;
use function intval;
use function is_array;
use function is_null;
use function is_string;
use const IPS\DEV_USE_FURL_CACHE;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Friendly URL
*/
class _Friendly extends \IPS\Http\Url\Internal
class Friendly extends Internal
{
/**
* @brief Base
*/
public $base = 'front';
public ?string $base = 'front';
/**
* @brief SEO Template
*/
public $seoTemplate;
public ?string $seoTemplate = NULL;
/**
* @brief SEO Titles (unencoded)
*/
public $seoTitles = array();
public array $seoTitles = array();
/**
* @brief The friendly URL component, which may be for the path or the query string (e.g. "topic/1-test")
*/
public $friendlyUrlComponent;
public ?string $friendlyUrlComponent;
/* !Factory methods */
/**
* Create a friendly URL from a query string
*
* @param int $protocol Protocol (one of the PROTOCOL_* constants)
* @param string $friendlyUrlComponent The friendly URL component, which may be for the path or the query string (e.g. "topic/1-test")
* @param array $queryString Additional query string data
* @param int $protocol Protocol (one of the PROTOCOL_* constants)
* @param string $friendlyUrlComponent The friendly URL component, which may be for the path or the query string (e.g. "topic/1-test")
* @param array|string $queryString Additional query string data
* @return static
* @throws \IPS\Http\Url\Exception
* @throws Exception
*/
public static function friendlyUrlFromComponent( $protocol, $friendlyUrlComponent, $queryString )
public static function friendlyUrlFromComponent( int $protocol, string $friendlyUrlComponent, array|string $queryString ): Url
{
if ( \IPS\Settings::i()->htaccess_mod_rewrite )
if ( Settings::i()->htaccess_mod_rewrite )
{
return static::createInternalFromComponents(
'front',
@@ -66,11 +84,20 @@ class _Friendly extends \IPS\Http\Url\Internal
}
else
{
if( is_string( $queryString ) )
{
parse_str( $queryString, $queryStringArray );
}
else
{
$queryStringArray = $queryString;
}
return static::createInternalFromComponents(
'front',
$protocol,
'index.php',
( $friendlyUrlComponent ? array( "/{$friendlyUrlComponent}/" => NULL ) : array() ) + $queryString
( $friendlyUrlComponent ? array( "/{$friendlyUrlComponent}/" => NULL ) : array() ) + $queryStringArray
);
}
}
@@ -78,11 +105,11 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Create a friendly URL from a full URL, working out friendly URL data
*
* @param array $components An array of components as returned by componentsFromUrlString()
* @param string $potentialFurl The potential FURL slug (e.g. "topic/1-test")
* @param array $components An array of components as returned by componentsFromUrlString()
* @param string $potentialFurl The potential FURL slug (e.g. "topic/1-test")
* @return static|NULL
*/
public static function createFriendlyUrlFromComponents( $components, $potentialFurl )
public static function createFriendlyUrlFromComponents( array $components, string $potentialFurl ): ?static
{
/* Loop each of our friendly URL definitions */
$matchedFurlDefinition = NULL;
@@ -92,7 +119,7 @@ class _Friendly extends \IPS\Http\Url\Internal
if ( $returnedMatches = static::getMatchedParamsFromFriendlyUrlComponent( $furlDefinition, $potentialFurl ) )
{
$matchedFurlDefinition = $furlDefinition;
list( $matchedParams, $seoTitles ) = $returnedMatches;
[ $matchedParams, $seoTitles ] = $returnedMatches;
break;
}
}
@@ -100,35 +127,100 @@ class _Friendly extends \IPS\Http\Url\Internal
/* If we didn't get one return NULL */
if ( !$matchedFurlDefinition )
{
return NULL;
$return = NULL;
}
/* If we did, create our object... */
return static::createFromComponents( $components[ static::COMPONENT_HOST ], $components[ static::COMPONENT_SCHEME ], $components[ static::COMPONENT_PATH ], $components[ static::COMPONENT_QUERY ], $components[ static::COMPONENT_PORT ], $components[ static::COMPONENT_USERNAME ], $components[ static::COMPONENT_PASSWORD ], $components[ static::COMPONENT_FRAGMENT ] )
->setFriendlyUrlData( $seoTemplate, $seoTitles, $matchedParams, $potentialFurl );
else if( $return = static::createFromComponents( $components[ static::COMPONENT_HOST ], $components[ static::COMPONENT_SCHEME ], $components[ static::COMPONENT_PATH ], $components[ static::COMPONENT_QUERY ], $components[ static::COMPONENT_PORT ], $components[ static::COMPONENT_USERNAME ], $components[ static::COMPONENT_PASSWORD ], $components[ static::COMPONENT_FRAGMENT ] )
->setFriendlyUrlData( $seoTemplate, $seoTitles, $matchedParams, $potentialFurl ) or !$potentialFurl )
{
if ( !Application::appIsEnabled('cms') or !Application::load('cms')->default OR $potentialFurl )
{
return $return;
}
}
if( Application::appIsEnabled('cms') )
{
/* Try to find a page */
try
{
[ $pagePath, $pageNumber ] = Page::getStrippedPagePath( $potentialFurl );
/* Is this from the static folders? */
if ( mb_substr( $pagePath, 0, 7 ) === 'static/' )
{
/* Yes, so just return to avoid looking it up in pages when it won't be there */
return $return;
}
try
{
$page = Page::loadFromPath( $pagePath );
}
catch( \Exception $e )
{
/* Try from furl */
try
{
$page = Page::load( Db::i()->select( 'store_current_id', 'cms_url_store', array( 'store_type=? and store_path=?', 'page', $pagePath ) )->first() );
}
catch( UnderflowException $e )
{
throw new OutOfRangeException;
}
}
return static::createFromComponents( $components[ static::COMPONENT_HOST ], $components[ static::COMPONENT_SCHEME ], $components[ static::COMPONENT_PATH ], $components[ static::COMPONENT_QUERY ], $components[ static::COMPONENT_PORT ], $components[ static::COMPONENT_USERNAME ], $components[ static::COMPONENT_PASSWORD ], $components[ static::COMPONENT_FRAGMENT ] )
->setFriendlyUrlData( 'content_page_path', array( $potentialFurl ), array( 'path' => $potentialFurl ), $potentialFurl );
}
/* Couldn't find one? Don't accept responsibility */
catch ( OutOfRangeException $e )
{
return $return;
}
/* The table may not yet exist if we're using the parser in an upgrade */
catch ( \Exception $e )
{
if( $e->getCode() == 1146 )
{
return $return;
}
throw $e;
}
}
return $return;
}
/**
* Create a friendly URL from a query string with known friendly URL data
*
* @param string $queryString The query string
* @param string $seoTemplate The key for making this a friendly URL
* @param string|array $seoTitles The title(s) needed for the friendly URL
* @param int $protocol Protocol (one of the PROTOCOL_* constants)
* @param string $queryString The query string
* @param string $seoTemplate The key for making this a friendly URL
* @param array|string $seoTitles The title(s) needed for the friendly URL
* @param int $protocol Protocol (one of the PROTOCOL_* constants)
* @return static
* @throws \IPS\Http\Url\Exception
* @throws Exception
* @todo Currently this will silently return a non-friendly URL if $seoTemplate is not valid, for backwards compatibility. Remove this in a major update.
*/
public static function friendlyUrlFromQueryString( $queryString, $seoTemplate, $seoTitles, $protocol )
public static function friendlyUrlFromQueryString( string $queryString, string $seoTemplate, array|string $seoTitles, int $protocol ): Url
{
$_originalQueryString = $queryString;
if ( $seoTemplate === 'content_page_path' )
{
/* Get the friendly URL component */
$friendlyUrlComponent = static::buildFriendlyUrlComponentFromData( $queryString, $seoTemplate, $seoTitles );
return static::friendlyUrlFromComponent( $protocol, $friendlyUrlComponent, $queryString )->setFriendlyUrlData( $seoTemplate, $seoTitles, array( 'path' => $friendlyUrlComponent ), $friendlyUrlComponent );
}
/* Get the friendly URL component */
try
{
$friendlyUrlComponent = static::buildFriendlyUrlComponentFromData( $queryString, $seoTemplate, $seoTitles );
}
catch ( \IPS\Http\Url\Exception $e )
catch (Exception $e )
{
if ( $e->getMessage() === 'INVALID_SEO_TEMPLATE' and !\IPS\IN_DEV )
{
@@ -139,23 +231,23 @@ class _Friendly extends \IPS\Http\Url\Internal
throw $e;
}
}
/* Extract the hidden query string parameters inside it */
$matchedParams = array();
$furlDefinition = static::furlDefinition();
if ( !isset( $furlDefinition[ $seoTemplate ] ) )
{
throw new \IPS\Http\Url\Exception( 'INVALID_SEO_TEMPLATE' );
throw new Exception( 'INVALID_SEO_TEMPLATE' );
}
if ( $returnedMatches = static::getMatchedParamsFromFriendlyUrlComponent( $furlDefinition[ $seoTemplate ], $friendlyUrlComponent ) )
{
list( $matchedParams, $_seoTitles ) = $returnedMatches;
[ $matchedParams, $_seoTitles ] = $returnedMatches;
}
else
{
if ( \IPS\IN_DEV )
{
throw new \IPS\Http\Url\Exception( 'SEO_TEMPLATE_IS_NOT_VALID_FOR_URL: ' . $_originalQueryString . ' - ' . $seoTemplate );
throw new Exception( 'SEO_TEMPLATE_IS_NOT_VALID_FOR_URL: ' . $_originalQueryString . ' - ' . $seoTemplate );
}
}
@@ -166,27 +258,42 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Set friendly URL data
*
* @param string $seoTemplate The key for making this a friendly URL
* @param string|array $seoTitles The title(s) needed for the friendly URL
* @param array $matchedParams The values for hidden query string properties
* @param string $seoTemplate The key for making this a friendly URL
* @param array|string $seoTitles The title(s) needed for the friendly URL
* @param array $matchedParams The values for hidden query string properties
* @param string $friendlyUrlComponent The friendly URL component, which may be for the path or the query string (e.g. "topic/1-test")
* @return static
* @throws \IPS\Http\Url\Exception
* @throws Exception
*/
protected function setFriendlyUrlData( $seoTemplate, $seoTitles, $matchedParams=array(), string $friendlyUrlComponent = '' )
protected function setFriendlyUrlData( string $seoTemplate, array|string $seoTitles, array $matchedParams=array(), string $friendlyUrlComponent = '' ): static
{
if ( $seoTemplate === 'content_page_path' )
{
/* Set basic properties */
$this->seoTemplate = 'content_page_path';
$this->seoTitles = is_string( $seoTitles ) ? array( $seoTitles ) : $seoTitles;
$this->friendlyUrlComponent = $friendlyUrlComponent;
$this->seoPagination = true;
/* Set hidden query string */
$this->hiddenQueryString = array( 'app' => 'cms', 'module' => 'pages', 'controller' => 'page' ) + $matchedParams;
/* Return */
return $this;
}
/* Get the definition */
$furlDefinition = static::furlDefinition();
if ( !isset( $furlDefinition[ $seoTemplate ] ) )
{
throw new \IPS\Http\Url\Exception( 'INVALID_SEO_TEMPLATE' );
throw new Exception( 'INVALID_SEO_TEMPLATE' );
}
/* Set basic properties */
$this->seoTemplate = $seoTemplate;
$this->seoTitles = \is_string( $seoTitles ) ? array( $seoTitles ) : $seoTitles;
$this->seoTitles = is_string( $seoTitles ) ? array( $seoTitles ) : $seoTitles;
$this->friendlyUrlComponent = $friendlyUrlComponent;
$this->seoPagination = ! empty( $furlDefinition[ $seoTemplate ]['seoPagination'] ) ? true : false;
$this->seoPagination = ! empty( $furlDefinition[ $seoTemplate ]['seoPagination'] );
/* Set hidden query string */
parse_str( $furlDefinition[ $seoTemplate ]['real'], $hiddenQueryString );
@@ -201,7 +308,7 @@ class _Friendly extends \IPS\Http\Url\Internal
*
* @return mixed The correct URL if incorrect, TRUE if correct, or NULL if unknown
*/
public function correctFriendlyUrl()
public function correctFriendlyUrl(): mixed
{
/* Get the definition */
$furlDefinition = static::furlDefinition();
@@ -262,7 +369,7 @@ class _Friendly extends \IPS\Http\Url\Internal
return NULL;
}
/* It doesn't exist */
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
return NULL;
}
@@ -271,9 +378,9 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Normalise the URL for comparing if the "correct" URL is different to the accessed URL
*
* @return \IPS\Http\Url
* @return Url
*/
public function normaliseForEqualityCheck()
public function normaliseForEqualityCheck(): Url
{
/* Strip the query string except the FURL component */
$return = $this->stripQueryString();
@@ -284,9 +391,6 @@ class _Friendly extends \IPS\Http\Url\Internal
/* Remove pagination attributes */
$return = $return->setPage();
/* now remove any trailing / from the path */
$return = $return->setPath( rtrim( $return->data['path'], '/' ) );
/* Return */
return $return;
}
@@ -295,12 +399,12 @@ class _Friendly extends \IPS\Http\Url\Internal
* Strip Query String
* Overrides main method so that the FURL slug isn't lost
*
* @param string|array $keys The key(s) to strip - if omitted, entire query string is wiped
* @return \IPS\Http\Url
* @param array|string|null $keys The key(s) to strip - if omitted, entire query string is wiped
* @return Url
*/
public function stripQueryString( $keys=NULL )
public function stripQueryString( array|string $keys=NULL ): Url
{
if ( $keys === NULL and !\IPS\Settings::i()->htaccess_mod_rewrite )
if ( $keys === NULL and !Settings::i()->htaccess_mod_rewrite )
{
$keys = $this->queryString;
unset( $keys[ "/{$this->friendlyUrlComponent}/" ] );
@@ -313,11 +417,11 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* 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 \IPS\Http\Url
* @param string $param The page key, default is 'page'
* @param int|null $number The page number to use
* @return Url
*/
public function setPage( $param='page', $number=1 )
public function setPage( string $param='page', ?int $number=1 ): Url
{
$number = (int) $number;
if ( $param === 'page' )
@@ -344,9 +448,9 @@ class _Friendly extends \IPS\Http\Url\Internal
}
/* If rewrite furls is disabled, or specifically if the request has index.php in the URL (it may be a cached URL from before the setting was toggled) */
if ( !\IPS\Settings::i()->htaccess_mod_rewrite OR static::fixComponentPath( $components[ static::COMPONENT_PATH ] ) === 'index.php' )
if ( !Settings::i()->htaccess_mod_rewrite OR static::fixComponentPath( $components[ static::COMPONENT_PATH ] ) === 'index.php' )
{
if ( \is_array( $components[ static::COMPONENT_QUERY ] ) and \count( $components[ static::COMPONENT_QUERY ] ) )
if ( is_array( $components[ static::COMPONENT_QUERY ] ) and count( $components[ static::COMPONENT_QUERY ] ) )
{
$new = NULL;
foreach( $components[ static::COMPONENT_QUERY ] as $k => $v )
@@ -377,10 +481,10 @@ class _Friendly extends \IPS\Http\Url\Internal
}
/* Add on the page itself */
if ( \intval( $number ) > 1 )
if ( $number > 1 )
{
$potentialFurl = NULL;
if ( ! \IPS\Settings::i()->htaccess_mod_rewrite OR static::fixComponentPath( $components[ static::COMPONENT_PATH ] ) === 'index.php' )
if ( ! Settings::i()->htaccess_mod_rewrite OR static::fixComponentPath( $components[ static::COMPONENT_PATH ] ) === 'index.php' )
{
foreach( $components[ static::COMPONENT_QUERY ] as $k => $v )
{
@@ -421,7 +525,7 @@ class _Friendly extends \IPS\Http\Url\Internal
*
* @return string|NULL The furl part of the URL, eg /topic/1-foo/
*/
protected function getFriendlyComponent()
protected function getFriendlyComponent(): ?string
{
/* Are we lucky? */
if ( ! empty( $this->friendlyUrlComponent ) )
@@ -430,9 +534,9 @@ class _Friendly extends \IPS\Http\Url\Internal
}
/* Of course not... */
if ( ! \IPS\Settings::i()->htaccess_mod_rewrite )
if ( ! Settings::i()->htaccess_mod_rewrite )
{
if ( \is_array( $this->queryString ) and \count( $this->queryString ) )
if ( is_array( $this->queryString ) and count( $this->queryString ) )
{
foreach( $this->queryString as $k => $v )
{
@@ -459,7 +563,7 @@ class _Friendly extends \IPS\Http\Url\Internal
* @param string $path The path from ->data['path'] eg forums/topic/1-foo
* @return string The furl part of the path, eg topic/1-foo
*/
public static function fixComponentPath( $path )
public static function fixComponentPath( string $path ): string
{
$baseUrl = parse_url( static::baseUrl() );
@@ -478,10 +582,10 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Returns the matching FURL definition data for the FURL component eg /topic/1-foo/
*
* @param string $friendlyUrlComponent The furl part of the path, eg /topic/1-foo/
* @param string $friendlyUrlComponent The furl part of the path, eg /topic/1-foo/
* @return array|NULL The FURL definition
*/
protected static function getFurlDefinitionFromPath( $friendlyUrlComponent )
protected static function getFurlDefinitionFromPath( string $friendlyUrlComponent ): ?array
{
foreach ( static::furlDefinition() as $seoTemplate => $furlDefinition )
{
@@ -500,11 +604,11 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Removes any inline page component from the FURL component eg /topic/1-foo/page/2/
*
* @param string $friendlyUrlComponent The furl part of the path, eg /topic/1-foo/
* @param string $param The /page/N/ key
* @param string $friendlyUrlComponent The furl part of the path, eg /topic/1-foo/
* @param string $param The /page/N/ key
* @return string The cleaned FURL eg /topic/1-foo/
*/
public static function stripPageComponent( $friendlyUrlComponent, $param='page' )
public static function stripPageComponent( string $friendlyUrlComponent, string $param='page' ): string
{
return preg_replace( '#/' . $param . '/(\d+?)$#', '', rtrim( $friendlyUrlComponent, '/' ) ) . '/';
}
@@ -512,39 +616,55 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Get friendly URL component (e.g. "topic/1-test") from a query string and SEO template
*
* @param string $queryString The query string - is passed by reference and any parts used are removed, which can be used to detect extraneous parts
* @param string $seoTemplate The key for making this a friendly URL
* @param string|array $seoTitles The title(s) needed for the friendly URL
* @param string $queryString The query string - is passed by reference and any parts used are removed, which can be used to detect extraneous parts
* @param string $seoTemplate The key for making this a friendly URL
* @param array|string|null $seoTitles The title(s) needed for the friendly URL
* @return string
* @throws \IPS\Http\Url\Exception
* @throws Exception
*/
public static function buildFriendlyUrlComponentFromData( &$queryString, $seoTemplate, $seoTitles )
public static function buildFriendlyUrlComponentFromData( string &$queryString, string $seoTemplate, array|string|null $seoTitles ): string
{
if ( $seoTemplate === 'content_page_path' )
{
parse_str( $queryString, $queryStringParts );
unset( $queryStringParts['app'] );
unset( $queryStringParts['module'] );
unset( $queryStringParts['controller'] );
unset( $queryStringParts['page'] );
$return = $queryStringParts['path'];
unset( $queryStringParts['path'] );
$queryString = http_build_query( $queryStringParts );
return $return;
}
/* Get the definition */
$furlDefinition = static::furlDefinition();
if ( !isset( $furlDefinition[ $seoTemplate ] ) )
{
throw new \IPS\Http\Url\Exception( 'INVALID_SEO_TEMPLATE' );
throw new Exception( 'INVALID_SEO_TEMPLATE ' . $seoTemplate );
}
$component = $furlDefinition[ $seoTemplate ]['friendly'];
/* Parse the query string into an array */
parse_str( $queryString, $queryString );
parse_str( $queryString, $queryStringParts );
/* For each query string component, replace it out */
foreach ( $queryString as $k => $v )
foreach ( $queryStringParts as $k => $v )
{
if ( mb_strpos( $component, "{#{$k}}" ) !== FALSE or mb_strpos( $component, "{@{$k}}" ) !== FALSE )
{
$component = str_replace( "{#{$k}}", \intval( $v ), $component );
$component = str_replace( "{#{$k}}", intval( $v ), $component );
$component = str_replace( "{@{$k}}", $v, $component );
unset( $queryString[ $k ] );
unset( $queryStringParts[ $k ] );
}
}
/* Parse out the titles */
$seoTitles = \is_null( $seoTitles ) ? array() : ( \is_string( $seoTitles ) ? array( $seoTitles ) : array_values( $seoTitles ) );
$seoTitlesCount = \count( $seoTitles );
$seoTitles = is_null( $seoTitles ) ? array() : ( is_string( $seoTitles ) ? array( $seoTitles ) : array_values( $seoTitles ) );
$seoTitlesCount = count( $seoTitles );
for ( $i = 0; $i < $seoTitlesCount; $i++ )
{
if ( $i === 0 )
@@ -558,11 +678,13 @@ class _Friendly extends \IPS\Http\Url\Internal
parse_str( $furlDefinition[ $seoTemplate ]['real'], $realQueryString );
foreach ( $realQueryString as $k => $v )
{
if ( isset( $queryString[ $k ] ) and $queryString[ $k ] == $v )
if ( isset( $queryStringParts[ $k ] ) and $queryStringParts[ $k ] == $v )
{
unset( $queryString[ $k ] );
unset( $queryStringParts[ $k ] );
}
}
$queryString = http_build_query( $queryStringParts );
/* Return */
return $component;
@@ -575,7 +697,7 @@ class _Friendly extends \IPS\Http\Url\Internal
* @return string
* @note Many places require an SEO title, so we always need to return something, so when no valid title is available we return a dash
*/
public static function seoTitle( $value )
public static function seoTitle( string $value ): string
{
/* Ensure there are no HTML tags */
$value = strip_tags( $value );
@@ -604,11 +726,11 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Get the matched parameters and SEO titles from a friendly URL component for a particular
*
* @param array $furlDefinition The FURL definition from the furl.json file
* @param string $friendlyUrlComponent The friendly URL component, which may be for the path or the query string (e.g. "topic/1-test")
* @param array $furlDefinition The FURL definition from the furl.json file
* @param string $friendlyUrlComponent The friendly URL component, which may be for the path or the query string (e.g. "topic/1-test")
* @return array|NULL array( $matchedParams, $seoTitles ) if matched, NULL if the $friendlyUrlComponent does not match this $furlDefinition
*/
protected static function getMatchedParamsFromFriendlyUrlComponent( $furlDefinition, $friendlyUrlComponent )
protected static function getMatchedParamsFromFriendlyUrlComponent( array $furlDefinition, string $friendlyUrlComponent ): ?array
{
/* See if it matches */
$seoTitles = array();
@@ -625,13 +747,13 @@ class _Friendly extends \IPS\Http\Url\Internal
{
foreach( $matches as $k => $v )
{
if ( $k and mb_strstr( $matches[ $k ], '/page/' ) )
if ( $k and mb_strstr( $v, '/page/' ) )
{
list( $x, $page ) = explode( '/', trim( $matches[ $k ], '/' ) );
[ $x, $page ] = explode( '/', trim( $v, '/' ) );
if ( $page )
{
$matchedParams['page'] = \intval( $page );
$matchedParams['page'] = intval( $page );
}
}
}
@@ -660,7 +782,7 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* @brief FURL Definition
*/
protected static $furlDefinition = NULL;
protected static ?array $furlDefinition = NULL;
/**
* Get FURL Definition
@@ -668,21 +790,26 @@ class _Friendly extends \IPS\Http\Url\Internal
* @param bool $revert If TRUE, ignores all customisations and reloads from json
* @return array
*/
public static function furlDefinition( $revert=FALSE )
public static function furlDefinition( bool $revert=FALSE ): array
{
if ( static::$furlDefinition === NULL or $revert )
{
$furlCustomizations = ( \IPS\Settings::i()->furl_configuration AND !$revert and ! \IPS\IN_DEV ) ? json_decode( \IPS\Settings::i()->furl_configuration, TRUE ) : array();
$furlConfiguration = ( isset( \IPS\Data\Store::i()->furl_configuration ) AND \IPS\Data\Store::i()->furl_configuration ) ? json_decode( \IPS\Data\Store::i()->furl_configuration, TRUE ) : array();
$furlCustomizations = ( Settings::i()->furl_configuration AND !$revert and ! \IPS\IN_DEV ) ? json_decode( Settings::i()->furl_configuration, TRUE ) : array();
$furlConfiguration = ( isset( Store::i()->furl_configuration ) AND Store::i()->furl_configuration ) ? json_decode( Store::i()->furl_configuration, TRUE ) : array();
if ( ( \IPS\IN_DEV and !\IPS\DEV_USE_FURL_CACHE ) or !\count( $furlConfiguration ) or $revert )
if ( ( \IPS\IN_DEV and !DEV_USE_FURL_CACHE ) or !count( $furlConfiguration ) or $revert )
{
$furlConfiguration = static::buildFurlConfiguation();
}
static::$furlDefinition = array_merge( $furlConfiguration, $furlCustomizations );
if( Application::appIsEnabled('cms') )
{
static::$furlDefinition= array_merge( static::$furlDefinition, array( 'content_page_path' => static::buildFurlDefinition( 'app=cms&module=pages&controller=page', 'app=cms&module=pages&controller=page', NULL, FALSE, NULL, FALSE, 'IPS\cms\Pages\Router' ) ) );
}
}
return static::$furlDefinition;
}
@@ -691,13 +818,13 @@ class _Friendly extends \IPS\Http\Url\Internal
*
* @return array
*/
protected static function buildFurlConfiguation()
protected static function buildFurlConfiguation(): array
{
/* Init */
$furlConfiguration = array();
/* Load apps, prioritising the default (otherwise if two apps both have "/category/..." the app which is higher in the list may steal from the default app) */
$applications = \IPS\Application::applications();
$applications = Application::applications();
foreach ( $applications as $k => $app )
{
if ( $app->default )
@@ -715,14 +842,14 @@ class _Friendly extends \IPS\Http\Url\Internal
if( $app->enabled and file_exists( $app->getApplicationPath() . "/data/furl.json" ) )
{
/* Open it up */
$data = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( $app->getApplicationPath() . "/data/furl.json" ) ), TRUE );
$data = json_decode( preg_replace( '/\/\*.+?\*\//s', '', file_get_contents( $app->getApplicationPath() . "/data/furl.json" ) ), TRUE );
$topLevel = $data['topLevel'];
/* Process them */
$definitions = array();
foreach ( $data['pages'] as $k => $page )
{
$definitions[ $k ] = static::buildFurlDefinition( $page['friendly'], $page['real'], $topLevel, $app->default, isset( $page['alias'] ) ? $page['alias'] : NULL, FALSE, isset( $page['verify'] ) ? $page['verify'] : NULL, isset( $page['seoTitles'] ) ? $page['seoTitles'] : NULL, isset( $page['seoPagination'] ) ? $page['seoPagination'] : NULL );
$definitions[ $k ] = static::buildFurlDefinition($page['friendly'], $page['real'], $topLevel, $app->default, $page['alias'] ?? NULL, FALSE, $page['verify'] ?? NULL, $page['seoTitles'] ?? NULL, $page['seoPagination'] ?? NULL );
}
/* Add it in */
@@ -731,7 +858,7 @@ class _Friendly extends \IPS\Http\Url\Internal
}
/* Store */
\IPS\Data\Store::i()->furl_configuration = json_encode( $furlConfiguration );
Store::i()->furl_configuration = json_encode( $furlConfiguration );
/* Return */
return $furlConfiguration;
@@ -740,18 +867,18 @@ class _Friendly extends \IPS\Http\Url\Internal
/**
* Build the friendly URL definition
*
* @param string $friendly Friendly URL pattern
* @param string $real Non-friendly URL pattern
* @param NULL|string $appTopLevel FURL slug if the app is not the default app
* @param bool $appIsDefault Flag to indicate if the app is default or not
* @param NULL|string $alias Friendly URL alias
* @param bool $custom Flag to indicate if this is a custom FURL definition
* @param string $verify The name of a class that contains a loadFromUrl() and an url() method for verifying the friendly URL is correct
* @param array $seoTitles The class, query param and property to load from to rebuild seo titles
* @param bool|null $seoPagination Whether to use SEO-friendly pagination (e.g. /page/2/) or not
* @param string $friendly Friendly URL pattern
* @param string $real Non-friendly URL pattern
* @param string|null $appTopLevel FURL slug if the app is not the default app
* @param bool $appIsDefault Flag to indicate if the app is default or not
* @param string|null $alias Friendly URL alias
* @param bool $custom Flag to indicate if this is a custom FURL definition
* @param string|null $verify The name of a class that contains a loadFromUrl() and an url() method for verifying the friendly URL is correct
* @param array|null $seoTitles The class, query param and property to load from to rebuild seo titles
* @param bool|null $seoPagination Whether to use SEO-friendly pagination (e.g. /page/2/) or not
* @return array
*/
public static function buildFurlDefinition( $friendly, $real, $appTopLevel = NULL, $appIsDefault = FALSE, $alias = NULL, $custom = FALSE, $verify = NULL, $seoTitles = NULL, $seoPagination = NULL )
public static function buildFurlDefinition( string $friendly, string $real, string $appTopLevel = NULL, bool $appIsDefault=FALSE, string $alias = NULL, bool $custom=FALSE, string $verify = NULL, array $seoTitles = NULL, bool $seoPagination = NULL ): array
{
/* Init */
$return = array(
+98 -49
View File
@@ -11,30 +11,46 @@
namespace IPS\Http\Url;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
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( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Internal URL
*/
class _Internal extends \IPS\Http\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 string|array|NULL $query Query
* @param string|NULL $fragment Fragment
* @return \IPS\Http\Url
* @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( $base, $protocol, $path, $query = NULL, $fragment = NULL )
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 ) );
$baseUrlData = parse_url( static::baseUrl( is_numeric( $protocol ) ? $protocol : NULL ) );
$pathToUse = '';
if ( isset( $baseUrlData['path'] ) and $baseUrlData['path'] )
@@ -52,12 +68,12 @@ class _Internal extends \IPS\Http\Url
$return = static::createFromComponents(
$baseUrlData['host'],
( \is_string( $protocol ) and !\is_numeric( $protocol ) ) ? $protocol : ( ( isset( $baseUrlData['scheme'] ) AND $protocol !== NULL ) ? $baseUrlData['scheme'] : NULL ),
( is_string( $protocol ) and !is_numeric( $protocol ) ) ? $protocol : ( ( isset( $baseUrlData['scheme'] ) AND $protocol !== NULL ) ? $baseUrlData['scheme'] : NULL ),
$pathToUse ? "/{$pathToUse}" : '',
$query,
isset( $baseUrlData['port'] ) ? $baseUrlData['port'] : NULL,
isset( $baseUrlData['user'] ) ? $baseUrlData['user'] : NULL,
isset( $baseUrlData['pass'] ) ? $baseUrlData['pass'] : NULL,
$baseUrlData['port'] ?? NULL,
$baseUrlData['user'] ?? NULL,
$baseUrlData['pass'] ?? NULL,
$fragment
);
@@ -68,21 +84,23 @@ class _Internal extends \IPS\Http\Url
/**
* @brief Does this URL have the SEO pagination (page/n/)?
*/
public $seoPagination = NULL;
public ?bool $seoPagination = NULL;
/**
* @brief Base
*/
public $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()
public function correctFriendlyUrl(): mixed
{
foreach ( \IPS\Http\Url\Friendly::furlDefinition() as $seoTemplate => $furlDefinition )
$return = FALSE;
foreach (Friendly::furlDefinition() as $seoTemplate => $furlDefinition )
{
if( mb_stripos( $this->data[ static::COMPONENT_QUERY ], $furlDefinition['real'] ) !== FALSE )
{
@@ -95,9 +113,6 @@ class _Internal extends \IPS\Http\Url
switch ( mb_substr( $tag, 0, 1 ) )
{
case '#':
$params[] = mb_substr( $tag, 1 );
break;
case '@':
$params[] = mb_substr( $tag, 1 );
break;
@@ -105,7 +120,7 @@ class _Internal extends \IPS\Http\Url
}
/* 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 ) ) ) )
if( count( array_diff( $params, array_keys( $this->queryString ) ) ) )
{
continue;
}
@@ -119,16 +134,15 @@ class _Internal extends \IPS\Http\Url
/* Load it */
try
{
if ( $return = $this->correctUrlFromVerifyClass( $furlDefinition['verify'] ) )
if ( ! $return = $this->correctUrlFromVerifyClass( $furlDefinition['verify'] ) )
{
return $return;
$return = NULL;
}
return NULL;
}
/* It doesn't exist */
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
return NULL;
$return = NULL;
}
}
/* If not, we have to build it ourself */
@@ -147,27 +161,62 @@ class _Internal extends \IPS\Http\Url
$seoTitles[] = $class::load( $this->queryString[ $queryParam ] )->$property;
}
catch ( \OutOfRangeException $e ) {}
catch ( OutOfRangeException $e ) {}
}
}
return \IPS\Http\Url::internal( $this->data['query'], 'front', $seoTemplate, $seoTitles );
$return = Url::internal( $this->data['query'], 'front', $seoTemplate, $seoTitles );
}
}
catch ( \Exception $e ) {}
catch ( Exception $e ) {}
}
}
return TRUE;
/* 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 )
{
$return = TRUE;
}
return $return;
}
/**
* Get the correct URL using a "verify" property from the FURL definition
*
* @param object $verify Class containing `preCorrectUrlFromVerifyClass()` method to check URL against
* @return NULL|\IPS\Http\Url
* @throws \OutOfRangeException
* @param mixed $verify Class containing `preCorrectUrlFromVerifyClass()` method to check URL against
* @return NULL|Url
* @throws OutOfRangeException
*/
protected function correctUrlFromVerifyClass( $verify )
protected function correctUrlFromVerifyClass( mixed $verify ): ?Url
{
/* Load it */
if ( method_exists( $verify, 'preCorrectUrlFromVerifyClass' ) )
@@ -179,11 +228,11 @@ class _Internal extends \IPS\Http\Url
/* 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 \IPS\Content or $contentObject instanceof \IPS\Member\Club )
if ( $contentObject instanceof Content or $contentObject instanceof Club )
{
$canView = $contentObject->canView();
}
elseif ( $contentObject instanceof \IPS\Node\Model )
elseif ( $contentObject instanceof Model )
{
$canView = $contentObject->can('view');
}
@@ -204,7 +253,7 @@ class _Internal extends \IPS\Http\Url
$paramsToSet[ $k ] = $v;
}
}
if ( \count( $paramsToSet ) )
if ( count( $paramsToSet ) )
{
$correctUrl = $correctUrl->setQueryString( $paramsToSet );
}
@@ -216,26 +265,26 @@ class _Internal extends \IPS\Http\Url
/**
* Add CSRF check to query string
*
* @return \IPS\Http\Url
* @return Url
*/
public function csrf()
public function csrf(): Url
{
return $this->setQueryString( 'csrfKey', \IPS\Session::i()->csrfKey );
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 \IPS\Http\Url
* @param string $param The page key, default is 'page'
* @param int|null $number The page number to use
* @return Url
*/
public function setPage( $param='page', $number=1 )
public function setPage( string $param='page', ?int $number=1 ): Url
{
/* Remove any existing page param */
$url = $this->stripQueryString( $param );
if ( \intval( $number ) > 1 )
if ( intval( $number ) > 1 )
{
$url = $this->setQueryString( $param, $number );
}
@@ -248,7 +297,7 @@ class _Internal extends \IPS\Http\Url
*
* @return string
*/
public function acpQueryString()
public function acpQueryString(): string
{
$queryString = $this->queryString;
unset( $queryString['csrf'] );
@@ -261,7 +310,7 @@ class _Internal extends \IPS\Http\Url
*
* @return bool
*/
public function openRedirect()
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';
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* @brief Raw (data) 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 October 2023
*/
namespace IPS\Http\Url;
use IPS\Http\Url;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
class Raw extends Url
{
const TYPE_PLAIN = 'text/plain';
const TYPE_SVG = 'image/svg+xml';
const TYPE_PNG = 'image/png';
const TYPE_JPG = 'image/jpeg';
const TYPE_HTML = 'text/html';
const TYPE_CSV = 'text/csv';
const TYPE_GIF = 'image/gif';
protected string $mimeType = Raw::TYPE_PLAIN;
protected string $rawData = '';
/**
* Creates a raw URL (e.g. a data URL)
* @param string $data
* @param string $mimeType
*/
public function __construct( string $data, string $mimeType=Raw::TYPE_PLAIN )
{
$this->setData( $data, $mimeType );
}
/**
* Get the url as a string when accessing the `url` property. This is because the raw url is often a replacement for a file attahcment, for which the url property is referenced
* @param string $key
* @return string|void
*/
public function __get( string $key )
{
if ($key === 'url') {
return (string) $this;
}
}
/**
* Set the data of this url
*
* @param string $data
* @param ?string $mimeType The mime type. Pass NULL to not override the currently in use mime type
*
* @return void
*/
public function setData( string $data, ?string $mimeType=null ) : void
{
$this->rawData = $data;
$this->mimeType = $mimeType ?? $this->mimeType;
}
/**
* Get the data
*
* @return string
*/
public function getData() : string
{
return $this->rawData;
}
/**
* @return string
*/
public function __toString() : string
{
$encoded = rawurlencode( $this->rawData );
return "data:{$this->mimeType},{$encoded}";
}
}