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

+75 -67
View File
@@ -12,39 +12,47 @@
namespace IPS\Http;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\File;
use IPS\Log;
use IPS\Output;
use function count;
use function defined;
use function strlen;
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;
}
/**
* HTTP Ranges Request/Response Class
*/
class _Ranges extends Response
class Ranges extends Response
{
/**
* @brief File object
*/
public $file = NULL;
public ?File $file = NULL;
/**
* @brief Is this a range request?
*/
public $ranged = FALSE;
public bool $ranged = FALSE;
/**
* Constructor
*
* @param \IPS\File $file File object we are sending
* @param int $throttle Throttle speed (kb/sec)
* @param File $file File object we are sending
* @param int $throttle Throttle speed (kb/sec)
* @return void
*/
public function __construct( $file, $throttle=0 )
public function __construct( File $file, int $throttle=0 )
{
/* Send some headers */
\IPS\Output::i()->sendHeader( 'Accept-Ranges: bytes' );
\IPS\Output::i()->sendHeader( "Content-Transfer-Encoding: binary" );
Output::i()->sendHeader( 'Accept-Ranges: bytes' );
Output::i()->sendHeader( "Content-Transfer-Encoding: binary" );
/* We will need the file size */
$filesize = $file->filesize();
@@ -52,12 +60,12 @@ class _Ranges extends Response
$range = 0;
$start_range = 0;
\IPS\Log::debug( "Range response initiated", 'ranges' );
Log::debug( "Range response initiated", 'ranges' );
/* Was this a ranged request? */
if( isset( $_SERVER['HTTP_RANGE'] ) )
{
\IPS\Log::debug( "This is a range request: " . $_SERVER['HTTP_RANGE'], 'ranges' );
Log::debug( "This is a range request: " . $_SERVER['HTTP_RANGE'], 'ranges' );
/* Get the range */
list( $a, $range ) = explode( "=", $_SERVER['HTTP_RANGE'] );
@@ -66,39 +74,39 @@ class _Ranges extends Response
$ranges = explode( ",", $range );
/* Did we only find one range? */
if( \count($ranges) == 1 )
if( count($ranges) == 1 )
{
\IPS\Log::debug( "Only one range detected", 'ranges' );
Log::debug( "Only one range detected", 'ranges' );
/* Get the actual range start and end */
list( $start_range, $end_range ) = explode( "-", $range );
/* If there is no start, we want the last x bytes */
if( \strlen( $start_range ) == 0 )
if( strlen( $start_range ) == 0 )
{
$size = $filesize - 1;
$length = $end_range + 1;
$length = (int) $end_range + 1;
}
/* Or did we want the first x bytes? */
else if( \strlen( $end_range ) == 0 )
else if( strlen( $end_range ) == 0 )
{
$size = $filesize - 1;
$length = $size - $start_range + 1;
$length = $size - (int) $start_range + 1;
}
/* Or a specific range with start and end specified */
else
{
$size = $end_range;
$length = $end_range - $start_range + 1;
$length = (int) $end_range - (int) $start_range + 1;
}
/* If the range is invalid we need to send a 416 header and let the client know the valid range */
if( $start_range > $size OR $end_range > $size )
{
\IPS\Log::debug( "Invalid range, responding with 416 header. Requested {$start_range} - {$end_range} out of {$size}", 'ranges' );
Log::debug( "Invalid range, responding with 416 header. Requested {$start_range} - {$end_range} out of {$size}", 'ranges' );
\IPS\Output::i()->sendStatusCodeHeader( 416 );
\IPS\Output::i()->sendHeader( "Content-Range: */{$filesize}" );
Output::i()->sendStatusCodeHeader( 416 );
Output::i()->sendHeader( "Content-Range: */{$filesize}" );
return;
}
/* Otherwise send the 206 header */
@@ -110,16 +118,16 @@ class _Ranges extends Response
$size = $filesize - 1;
}
\IPS\Log::debug( "Sending requested range with 206 header: {$start_range} - {$size} out of {$filesize}", 'ranges' );
Log::debug( "Sending requested range with 206 header: {$start_range} - {$size} out of {$filesize}", 'ranges' );
\IPS\Output::i()->sendStatusCodeHeader( 206 );
\IPS\Output::i()->sendHeader( "Content-Range: bytes {$start_range}-{$size}/{$filesize}" );
Output::i()->sendStatusCodeHeader( 206 );
Output::i()->sendHeader( "Content-Range: bytes {$start_range}-{$size}/{$filesize}" );
}
/* A couple more headers for good measure */
if( !ini_get('zlib.output_compression') OR ini_get('zlib.output_compression') == 'off' )
{
\IPS\Output::i()->sendHeader( "Content-Length: " . $filesize );
Output::i()->sendHeader( "Content-Length: " . $filesize );
}
}
/* Or did they get froggy and request multiple ranges? */
@@ -130,37 +138,37 @@ class _Ranges extends Response
foreach( $ranges as $arange )
{
\IPS\Log::debug( "Multiple ranges: {$start_range} - {$end_range}", 'ranges' );
/* Get the actual range start and end */
/* Get the actual range start and end */
list( $start_range, $end_range ) = explode( "-", $arange );
Log::debug( "Multiple ranges: {$start_range} - {$end_range}", 'ranges' );
/* If there is no start, we want the last x bytes */
if( \strlen( $start_range ) == 0 )
if( strlen( $start_range ) == 0 )
{
$size = $filesize - 1;
$length = $end_range + 1;
$length = (int) $end_range + 1;
}
/* Or did we want the first x bytes? */
else if( \strlen( $end_range ) == 0 )
else if( strlen( $end_range ) == 0 )
{
$size = $filesize - 1;
$length = $size - $start_range + 1;
$length = $size -(int) $start_range + 1;
}
/* Or a specific range with start and end specified */
else
{
$size = $end_range;
$length = $end_range - $start_range + 1;
$length = (int) $end_range - (int) $start_range + 1;
}
/* If the range is invalid we need to send a 416 header and let the client know the valid range */
if( $start_range > $size OR $end_range > $size )
{
\IPS\Log::debug( "This range out of multiple ranges was invalid", 'ranges' );
Log::debug( "This range out of multiple ranges was invalid", 'ranges' );
\IPS\Output::i()->sendStatusCodeHeader( 416 );
\IPS\Output::i()->sendHeader( "Content-Range: */{$filesize}" );
Output::i()->sendStatusCodeHeader( 416 );
Output::i()->sendHeader( "Content-Range: */{$filesize}" );
return;
}
/* Otherwise store this range */
@@ -171,7 +179,7 @@ class _Ranges extends Response
}
/* Did we still only have one range? */
if( \count($the_responses) == 1 )
if( count($the_responses) == 1 )
{
$length = $the_responses[0][2];
$start_range = $the_responses[0][0];
@@ -179,40 +187,40 @@ class _Ranges extends Response
$the_responses = array();
\IPS\Log::debug( "One range out of multiple ranges to respond: {$start_range} - {$size} out of {$filesize}", 'ranges' );
Log::debug( "One range out of multiple ranges to respond: {$start_range} - {$size} out of {$filesize}", 'ranges' );
\IPS\Output::i()->sendStatusCodeHeader( 206 );
\IPS\Output::i()->sendHeader( "Content-Range: bytes {$start_range}-{$size}/{$filesize}" );
Output::i()->sendStatusCodeHeader( 206 );
Output::i()->sendHeader( "Content-Range: bytes {$start_range}-{$size}/{$filesize}" );
/* A couple more headers for good measure */
if( !ini_get('zlib.output_compression') OR ini_get('zlib.output_compression') == 'off' )
{
\IPS\Output::i()->sendHeader( "Content-Length: " . $filesize );
Output::i()->sendHeader( "Content-Length: " . $filesize );
}
}
/* Now we're working with something exciting */
else if( \count($the_responses) > 1 )
else if( count($the_responses) > 1 )
{
$content_length = 0;
\IPS\Log::debug( "Multiple range responses, using boundary", 'ranges' );
Log::debug( "Multiple range responses, using boundary", 'ranges' );
foreach( $the_responses as $part )
{
$content_length += \strlen( "\r\n--IPDOWNLOADSBOUNDARYMARKER\r\n" );
$content_length += \strlen( "Content-Type: " . \IPS\File::getMimeType( $file->originalFilename ) . "\r\n" );
$content_length += \strlen( "Content-Range: bytes {$part[0]}-{$part[1]}/{$filesize}\r\n\r\n" );
$content_length += strlen( "\r\n--IPDOWNLOADSBOUNDARYMARKER\r\n" );
$content_length += strlen( "Content-Type: " . File::getMimeType( $file->originalFilename ) . "\r\n" );
$content_length += strlen( "Content-Range: bytes {$part[0]}-{$part[1]}/{$filesize}\r\n\r\n" );
$content_length += $part[2];
}
$content_length += \strlen( "\r\n--IPDOWNLOADSBOUNDARYMARKER--\r\n" );
$content_length += strlen( "\r\n--IPDOWNLOADSBOUNDARYMARKER--\r\n" );
\IPS\Output::i()->sendStatusCodeHeader( 206 );
\IPS\Output::i()->sendHeader( "Content-Type: multipart/x-byteranges; boundary=IPDOWNLOADSBOUNDARYMARKER" );
Output::i()->sendStatusCodeHeader( 206 );
Output::i()->sendHeader( "Content-Type: multipart/x-byteranges; boundary=IPDOWNLOADSBOUNDARYMARKER" );
if( !ini_get('zlib.output_compression') OR ini_get('zlib.output_compression') == 'off' )
{
\IPS\Output::i()->sendHeader( "Content-Length: " . $content_length );
Output::i()->sendHeader( "Content-Length: " . $content_length );
}
}
}
@@ -220,25 +228,25 @@ class _Ranges extends Response
/* No range */
else
{
\IPS\Log::debug( "No range requested, sending entire file", 'ranges' );
Log::debug( "No range requested, sending entire file", 'ranges' );
$size = $filesize - 1;
\IPS\Output::i()->sendStatusCodeHeader( 200 );
Output::i()->sendStatusCodeHeader( 200 );
if( !ini_get('zlib.output_compression') OR ini_get('zlib.output_compression') == 'off' )
{
\IPS\Output::i()->sendHeader( "Content-Length: " . $filesize );
Output::i()->sendHeader( "Content-Length: " . $filesize );
}
\IPS\Output::i()->sendHeader( "Content-Range: bytes 0-{$size}/{$filesize}" );
Output::i()->sendHeader( "Content-Range: bytes 0-{$size}/{$filesize}" );
}
/* Not sending multiple ranges */
if( !isset( $the_responses ) OR !\count( $the_responses ) )
if( !isset( $the_responses ) OR !count( $the_responses ) )
{
\IPS\Output::i()->sendHeader( 'Content-Type: ' . \IPS\File::getMimeType( $file->originalFilename ) );
\IPS\Output::i()->sendHeader( 'Content-Disposition: ' . \IPS\Output::getContentDisposition( 'attachment', $file->originalFilename ) );
Output::i()->sendHeader( 'Content-Type: ' . File::getMimeType( $file->originalFilename ) );
Output::i()->sendHeader( 'Content-Disposition: ' . Output::getContentDisposition( 'attachment', $file->originalFilename ) );
}
/* Turn off output buffering if it is on */
@@ -251,7 +259,7 @@ class _Ranges extends Response
$throttle = ( $throttle > 0 ) ? ( $throttle * 1024 ) : NULL;
/* Do we have multiple ranges? */
if( isset( $the_responses ) AND \count($the_responses) )
if( isset( $the_responses ) AND count($the_responses) )
{
/* Loop over each range */
foreach( $the_responses as $part )
@@ -259,7 +267,7 @@ class _Ranges extends Response
$length = $part[2];
echo "\r\n--IPDOWNLOADSBOUNDARYMARKER\r\n";
echo "Content-Type: " . \IPS\File::getMimeType( $file->originalFilename ) . "\r\n";
echo "Content-Type: " . File::getMimeType( $file->originalFilename ) . "\r\n";
echo "Content-Range: bytes {$part[0]}-{$part[1]}/{$filesize}\r\n\r\n";
$file->printFile( $part[0], $part[2], $throttle );
@@ -279,7 +287,7 @@ class _Ranges extends Response
*
* @return bool
*/
public static function isStartOfFile()
public static function isStartOfFile(): bool
{
/* Was this a ranged request? */
if( isset( $_SERVER['HTTP_RANGE'] ) )
@@ -291,22 +299,22 @@ class _Ranges extends Response
$ranges = explode( ",", $range );
/* Did we only find one range? */
if( \count($ranges) == 1 )
if( count($ranges) == 1 )
{
/* Get the actual range start and end */
list( $start_range, $end_range ) = explode( "-", $range );
if( \strlen( $start_range ) == 0 )
if( strlen( $start_range ) == 0 )
{
return TRUE;
}
else if( \strlen( $end_range ) == 0 )
else if( strlen( $end_range ) == 0 )
{
return FALSE;
}
else
{
return ( $start_range == 0 ) ? TRUE : FALSE;
return ( $start_range == 0 );
}
}
/* Or did they get froggy and request multiple ranges? */
@@ -317,11 +325,11 @@ class _Ranges extends Response
/* Get the actual range start and end */
list( $start_range, $end_range ) = explode( "-", $arange );
if( \strlen( $start_range ) == 0 )
if( strlen( $start_range ) == 0 )
{
return TRUE;
}
else if( \strlen( $end_range ) == 0 )
else if( strlen( $end_range ) == 0 )
{
continue;
}
+286 -87
View File
@@ -11,75 +11,130 @@
namespace IPS\Http\Request;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use CurlHandle;
use CurlMultiHandle;
use IPS\Application;
use IPS\Http\Response;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Log;
use Pdp\Domain;
use Pdp\Rules;
use function array_replace;
use function curl_setopt;
use function define;
use function defined;
use function in_array;
use function intval;
use function is_array;
use function is_int;
use function parse_url;
use const CURLOPT_WRITEFUNCTION;
use const IPS\DEBUG_LOG;
use const PHP_URL_HOST;
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;
}
/**
* cURL REST Class
*
* @method put( mixed $data = NULL ): Response
* @method delete( mixed $data = NULL ): Response
*/
class _Curl
class Curl
{
/**
* @brief URL
*/
protected $url = NULL;
protected ?Url $url = NULL;
/**
* @brief Curl Handle
*/
protected $curl = NULL;
protected null|false|CurlHandle $curl = NULL;
/**
* @brief Has the Content-Type header been set?
* @note Because cURL will automatically set the Content-Type header to multipart/form-data if we send a POST request with an array, we need to change it to a string if we want to send a different Content-Type
* @see <a href='http://www.php.net/manual/en/function.curl-setopt.php'>PHP: curl_setopt - Manual</a>
*/
protected $modifiedContentType = FALSE;
protected bool $modifiedContentType = FALSE;
/**
* @brief HTTP Version
*/
protected $httpVersion = '1.1';
protected string $httpVersion = '1.1';
/**
* @brief Timeout
*/
protected $timeout = 5;
protected int $timeout = 5;
/**
* @brief Follow redirects?
*/
protected $followRedirects = TRUE;
protected int|bool $followRedirects = TRUE;
/**
* @brief Allowed protocols
*/
protected $allowedProtocols = array();
protected array $allowedProtocols = array();
/**
* @brief Data sent
*/
protected $dataForLog = NULL;
protected mixed $dataForLog = NULL;
/**
* @brief Headers sent
*/
protected $headersForLog = [];
protected array $headersForLog = [];
/**
* @brief Flag used to show this request is going to the internal cloud server
*/
public bool $internalSignedRequest = false;
/**
* @brief Limit how much data we fetch
*/
protected int|null $bytesToGet = null;
/**
* @brief Untrusted endpoint, redirect only if the host is the same, so google.com > www.google.com is ok, but google.com > badActor.com is not
*/
protected bool $untrusted = false;
/**
* @brief Allowed content types (null allows all)
*/
protected array|null $allowedContentTypes = NULL;
/**
* @var array|null
*/
public array|null $cicHeaders = null;
/**
* Contructor
*
* @param \IPS\Http\Url $url URL
* @param int $timeout Timeout (in seconds)
* @param string $httpVersion HTTP Version
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
* @param array|null $allowedProtocols Protocols allowed (if NULL we default to array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' ))
* @param Url $url URL
* @param int $timeout Timeout (in seconds)
* @param string|null $httpVersion HTTP Version
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
* @param array|null $allowedProtocols Protocols allowed (if NULL we default to array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' ))
* @param array|null $allowedContentTypes Allowed content types (if null, it will allow all)
* @param bool $untrusted Untrusted endpoint, redirect only if the host is the same, so google.com > www.google.com is ok, but google.com > badActor.com is not
* @param int|null $bytesToGet Number of bytes to get. null means get everything you greedy piggie
*
* @return void
*/
public function __construct( $url, $timeout=5, $httpVersion=NULL, $followRedirects=TRUE, $allowedProtocols=NULL )
public function __construct( Url $url, int $timeout=5, string $httpVersion=NULL, bool|int $followRedirects=TRUE, array $allowedProtocols=NULL, array $allowedContentTypes=NULL, bool $untrusted=false, int|null $bytesToGet = null)
{
/* Init */
$this->url = $url;
@@ -88,6 +143,9 @@ class _Curl
$this->timeout = $timeout;
$this->followRedirects = $followRedirects;
$this->allowedProtocols = $allowedProtocols ?: array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' );
$this->allowedContentTypes = $allowedContentTypes;
$this->bytesToGet = $bytesToGet;
$this->untrusted = $untrusted;
/* Need to adjust if this is FTP */
$user = null;
@@ -160,7 +218,7 @@ class _Curl
public function __destruct()
{
curl_close( $this->curl );
if( static::$_multiHandle instanceof \CurlMultiHandle )
if( static::$_multiHandle instanceof CurlMultiHandle )
{
curl_multi_close( static::$_multiHandle );
}
@@ -169,11 +227,11 @@ class _Curl
/**
* Login
*
* @param string $username Username
* @param string $password Password
* @param string $username Username
* @param string $password Password
* @return static
*/
public function login( $username, $password )
public function login( string $username, string $password ): static
{
curl_setopt_array( $this->curl, array(
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
@@ -186,11 +244,19 @@ class _Curl
/**
* Set Headers
*
* @param array $headers Key/Value pair of headers
* @param array $headers Key/Value a pair of headers
* @return static
*/
public function setHeaders( $headers )
public function setHeaders( array $headers ): static
{
if ( Application::appIsEnabled('cloud') )
{
if ( $this->internalSignedRequest and isset( $this->cicHeaders ) )
{
$headers = array_replace( $headers, $this->cicHeaders );
}
}
$extra = array();
foreach ( $headers as $k => $v )
@@ -237,14 +303,14 @@ class _Curl
/**
* Toggle SSL checks
*
* @param boolean $value True will enable SSL checks, false will disable them
* @param boolean $value True will enable SSL checks, false will disable them
* @return static
*/
public function sslCheck( $value=TRUE )
public function sslCheck( bool $value=TRUE ): static
{
curl_setopt_array( $this->curl, array(
CURLOPT_SSL_VERIFYHOST => ( $value ) ? 2 : FALSE,
CURLOPT_SSL_VERIFYPEER => (boolean) $value
CURLOPT_SSL_VERIFYPEER => $value
) );
return $this;
@@ -255,24 +321,24 @@ class _Curl
*
* @return static
*/
public function forceTls()
public function forceTls(): static
{
$curlVersionData = curl_version();
if ( preg_match( '/^OpenSSL\/(\d+\.\d+\.\d+)/', $curlVersionData['ssl_version'], $openSSLVersionData ) )
{
if ( version_compare( $openSSLVersionData[1], '1.0.1', '>=' ) )
{
if ( !\defined('CURL_SSLVERSION_TLSv1_2') ) // constant not defined in PHP < 5.5
if ( !defined('CURL_SSLVERSION_TLSv1_2') ) // constant not defined in PHP < 5.5
{
\define( 'CURL_SSLVERSION_TLSv1_2', 6 );
define( 'CURL_SSLVERSION_TLSv1_2', 6 );
}
curl_setopt( $this->curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2 );
}
else
{
if ( !\defined('CURL_SSLVERSION_TLSv1') ) // constant not defined in PHP < 5.5
if ( !defined('CURL_SSLVERSION_TLSv1') ) // constant not defined in PHP < 5.5
{
\define( 'CURL_SSLVERSION_TLSv1', 1 );
define( 'CURL_SSLVERSION_TLSv1', 1 );
}
curl_setopt( $this->curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1 );
}
@@ -284,11 +350,11 @@ class _Curl
/**
* HTTP GET
*
* @param mixed $data Data to send with the GET request
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
* @param mixed|null $data Data to send with the GET request
* @return Response
* @throws CurlException
*/
public function get( $data=NULL )
public function get( mixed $data=NULL ): Response
{
/* Specify that this is a GET request */
curl_setopt( $this->curl, CURLOPT_HTTPGET, TRUE );
@@ -299,18 +365,26 @@ class _Curl
curl_setopt( $this->curl, CURLOPT_POSTFIELDS, $data );
}
return $this->_executeAndFollowRedirects( 'GET', NULL );
return $this->_executeAndFollowRedirects( 'GET' );
}
/**
* HTTP POST
*
* @param mixed $data Data to post (can be array or string)
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
* @param mixed|null $data Data to post (can be array or string)
* @return Response
* @throws CurlException
*/
public function post( $data=NULL )
{
public function post( mixed $data=NULL ): Response
{
if ( Application::appIsEnabled('cloud') )
{
if ( $this->internalSignedRequest )
{
$data = \IPS\Cicloud\prepareInternalPayload( $data );
}
}
/* Specify that this is a POST request */
curl_setopt( $this->curl, CURLOPT_POST, TRUE );
@@ -324,10 +398,10 @@ class _Curl
/**
* HTTP HEAD
*
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
* @return Response
* @throws CurlException
*/
public function head()
public function head(): Response
{
/* Specify the request method */
curl_setopt( $this->curl, CURLOPT_CUSTOMREQUEST, 'HEAD' );
@@ -336,19 +410,19 @@ class _Curl
curl_setopt( $this->curl, CURLOPT_NOBODY, true );
/* Execute */
return $this->_executeAndFollowRedirects( 'HEAD', NULL );
return $this->_executeAndFollowRedirects( 'HEAD' );
}
/**
* Magic Method: __call
* Used for other HTTP methods (like PUT and DELETE)
*
* @param string $method Method (A HTTP method)
* @param array $params Parameters (a single parameter with data to post, which can be an array or a string)
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
* @param string $method Method (A HTTP method)
* @param array $params Parameters (a single parameter with data to post, which can be an array or a string)
* @return Response
* @throws CurlException
*/
public function __call( $method, $params )
public function __call( string $method, array $params )
{
/* Specify the request method */
curl_setopt( $this->curl, CURLOPT_CUSTOMREQUEST, mb_strtoupper( $method ) );
@@ -366,13 +440,13 @@ class _Curl
/**
* Data to send
*
* @param mixed $data Data to post (can be array or string)
* @param mixed|null $data Data to post (can be array or string)
* @return mixed
*/
protected function _dataToSend( $data=NULL )
protected function _dataToSend( mixed $data=NULL ): mixed
{
$this->dataForLog = $data;
if ( !$this->modifiedContentType and \is_array( $data ) )
if ( !$this->modifiedContentType and is_array( $data ) )
{
$data = http_build_query( $data, '', '&' );
}
@@ -382,19 +456,61 @@ class _Curl
/**
* Execute the request
*
* @todo Remove if multi handle works out long term.
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
* @return Response
* @throws CurlException
*@todo Remove if multi handle works out long term.
*/
protected function _execute()
protected function _execute(): Response
{
/* Execute */
$output = curl_exec( $this->curl );
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
if ( $this->bytesToGet !== null )
{
\IPS\Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
/* Why not CURLOPT_RANGE? Servers can choose to ignore it (https://curl.se/libcurl/c/CURLOPT_RANGE.html) */
$data = '';
$headerProcessed = false;
$headers = '';
$bytesToGet = $this->bytesToGet;
curl_setopt( $this->curl, CURLOPT_WRITEFUNCTION, function( $ch, $chunk ) use ( &$data, &$headerProcessed, &$headers, $bytesToGet )
{
if ( ! $headerProcessed )
{
// Find the end of the headers
$endOfHeaders = strpos( $chunk, "\r\n" );
if ( $endOfHeaders !== false )
{
$headerProcessed = true; // Now start processing the content
}
else
{
$headers .= $chunk;
return strlen( $chunk ); // No headers ending found, skip this part of data
}
}
$length = strlen( $data ) + strlen( $chunk );
if ( $length >= $bytesToGet )
{
$data .= substr( $chunk, 0, $bytesToGet - strlen( $data ) );
return 0;
}
$data .= $chunk;
return strlen( $chunk );
} );
curl_exec( $this->curl );
$output = $headers . $data;
}
else
{
/* Execute */
$output = curl_exec( $this->curl );
}
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
if ( defined('\IPS\DEBUG_LOG') and DEBUG_LOG )
{
Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
}
/* Errors? */
@@ -410,22 +526,62 @@ class _Curl
}
/* Return it */
return new \IPS\Http\Response( $output );
$response = new Response( $output );
if ( ! $this->isContentTypeValid( $response ) )
{
throw new CurlException( 'Invalid content type' );
}
return $response;
}
/**
* Is the returned content type valid?
*
* @param Response $response
* @return bool
*/
protected function isContentTypeValid( Response $response ): bool
{
if ( $this->allowedContentTypes !== null and $response->httpHeaders !== null )
{
$headers = array_change_key_case( $response->httpHeaders, CASE_LOWER );
$contentType = $headers['content-type'] ?? 'unknown/unknown';
if ( strstr( $contentType, ';' ) )
{
$contentType = explode( ';', $contentType );
$contentType = $contentType[0];
}
if ( ! in_array( strtolower( $contentType ), $this->allowedContentTypes ) )
{
return false;
}
}
return true;
}
/**
* @breif Store the cURL Multi Handle
*/
protected static $_multiHandle;
protected static null|false|CurlMultiHandle $_multiHandle = NULL;
/**
* Execute the request via cURL Multi - We use this to cache and share connections between requests
*
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
* @return Response
* @throws CurlException
*/
protected function _executeMh(): \IPS\Http\Response
protected function _executeMh(): Response
{
/* If we're restricting by bytes, then we need to use the plain execute */
if( $this->bytesToGet !== null )
{
return $this->_execute();
}
/* Init multi handle if needed */
if( empty( static::$_multiHandle ) )
{
@@ -436,7 +592,7 @@ class _Curl
curl_multi_add_handle( static::$_multiHandle, $this->curl );
/* Execute request and wait for response */
$active = NULL;
$active = 0;
do
{
$ret = curl_multi_exec( static::$_multiHandle, $active );
@@ -461,9 +617,9 @@ class _Curl
curl_multi_remove_handle( static::$_multiHandle, $this->curl );
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
if ( defined('\IPS\DEBUG_LOG') and DEBUG_LOG )
{
\IPS\Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
}
/* Errors? */
@@ -479,24 +635,31 @@ class _Curl
}
/* Return it */
return new \IPS\Http\Response( $output );
$response = new Response( $output );
if ( ! $this->isContentTypeValid( $response ) )
{
throw new CurlException( 'Invalid content type' );
}
return $response;
}
/**
* Execute the request and follow redirects id necessary
*
* @param string $method Request method to use
* @param NULL|array $params Parameters to send with request
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
* @param string $method Request method to use
* @param array|null $params Parameters to send with request
* @return Response
* @throws CurlException
*/
protected function _executeAndFollowRedirects( $method, $params=NULL )
protected function _executeAndFollowRedirects( string $method, mixed $params=NULL ): Response
{
/* Execute */
$response = $this->_executeMh();
/* Either return it or follow it */
if ( $this->followRedirects and \in_array( $response->httpResponseCode, array( 301, 302, 303, 307, 308 ) ) )
if ( $this->followRedirects and in_array( $response->httpResponseCode, array( 301, 302, 303, 307, 308 ) ) )
{
/* Fix missing hostname in location */
foreach( $response->httpHeaders as $k => $v )
@@ -507,19 +670,54 @@ class _Curl
}
}
if( parse_url( $location, PHP_URL_HOST ) === NULL )
if( isset( $location ) and parse_url( $location, PHP_URL_HOST ) === NULL )
{
$location = $this->url->data['scheme'] . '://' . $this->url->data['host'] . $location;
}
$newRequest = \IPS\Http\Url::external( $location );
if( !\in_array( $newRequest->data['scheme'], $this->allowedProtocols ) )
if ( isset( $location ) and $this->untrusted )
{
throw new \IPS\Http\Request\Exception( 'protocol_not_followed' );
/* We want to make sure that the main domain is the same. So domain.com/foo can redirect to domain.com/newfoo and domain.co.uk can redirect to www.domain.co.uk but domain.co.uk cannot redirect to anotherdomain.com */
$pass = false;
if ( strtolower( $this->url->data['host'] ) === strtolower( parse_url( $location, PHP_URL_HOST ) ) )
{
// 1: Do a simple test: sub.domain.tld/foo -> sub.domain.tld/bar
$pass = true;
}
else
{
// 2: Now we need to check for sub.domain.tld to www.domain.tld, or domain.tld to www.domain.tld
IPS::$PSR0Namespaces['Pdp'] = \IPS\ROOT_PATH .'/system/3rd_party/Pdp';
$publicSuffixList = Rules::fromPath( \IPS\ROOT_PATH .'/system/3rd_party/Pdp/tlds.dat' );
$newDomain = Domain::fromIDNA2008( parse_url( $location, PHP_URL_HOST ) );
$result = $publicSuffixList->resolve( $newDomain );
$newLocation = $result->registrableDomain()->toString();
$oldDomain = Domain::fromIDNA2008( $this->url->data['host'] );
$result = $publicSuffixList->resolve( $oldDomain );
$oldLocation = $result->registrableDomain()->toString();
if ( strtolower( $oldLocation ) === strtolower( $newLocation ) )
{
$pass = true;
}
}
if ( ! $pass )
{
throw new Exception( 'untrusted_redirect' );
}
}
$newRequest = $newRequest->request( $this->timeout, $this->httpVersion, \is_int( $this->followRedirects ) ? ( $this->followRedirects - 1 ) : $this->followRedirects );
$newRequest = Url::external( $location );
if( !in_array( $newRequest->data['scheme'], $this->allowedProtocols ) )
{
throw new Exception( 'protocol_not_followed' );
}
$newRequest = $newRequest->request( $this->timeout, $this->httpVersion, is_int( $this->followRedirects ) ? ( $this->followRedirects - 1 ) : $this->followRedirects );
return $newRequest->$method( $params );
}
return $response;
@@ -529,4 +727,5 @@ class _Curl
/**
* CURL Exception Class
*/
class CurlException extends \IPS\Http\Request\Exception { }
class CurlException extends Exception
{ }
+7 -3
View File
@@ -11,13 +11,17 @@
namespace IPS\Http\Request;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use RuntimeException;
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;
}
/**
* Request Exception Class
*/
class _Exception extends \RuntimeException { }
class Exception extends RuntimeException { }
-309
View File
@@ -1,309 +0,0 @@
<?php
/**
* @brief Sockets REST Class
* @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 18 Mar 2013
*/
namespace IPS\Http\Request;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Sockets REST Class
*/
class _Sockets
{
/**
* @brief URL
*/
protected $url = NULL;
/**
* @brief Stream context
*/
protected $context;
/**
* @brief HTTP Version
*/
protected $httpVersion = '1.1';
/**
* @brief Timeout
*/
protected $timeout = 5;
/**
* @brief Headers
*/
protected $headers = array();
/**
* @brief Follow redirects?
*/
protected $followRedirects = TRUE;
/**
* @brief Allowed protocols
*/
protected $allowedProtocols = array();
/**
* Contructor
*
* @param \IPS\Http\Url $url URL
* @param int $timeout Timeout (in seconds)
* @param string $httpVersion HTTP Version
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
* @param array|null $allowedProtocols Protocols allowed (if NULL we default to array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' ))
* @return void
*/
public function __construct( $url, $timeout=5, $httpVersion=NULL, $followRedirects=TRUE, $allowedProtocols=NULL )
{
$this->url = $url;
$this->context = stream_context_create();
$this->httpVersion = $httpVersion ?: '1.1';
$this->timeout = $timeout;
$this->followRedirects = $followRedirects;
$this->allowedProtocols = $allowedProtocols ?: array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' );
/* Set our basic settings */
stream_context_set_option( $this->context, array(
'http' => array(
'protocol_version' => $httpVersion,
'follow_location' => $followRedirects,
'timeout' => $timeout,
'ignore_errors' => TRUE,
),
'ssl' => array(
'verify_peer' => FALSE,
'crypto_method' => STREAM_CRYPTO_METHOD_ANY_CLIENT
)
) );
}
/**
* Login
*
* @param string $username Username
* @param string $password Password
* @return static (for daisy chaining)
*/
public function login( $username, $password )
{
$this->setHeaders( array( 'Authorization' => 'Basic ' . base64_encode( "{$username}:{$password}" ) ) );
return $this;
}
/**
* Set Headers
*
* @param array $headers Key/Value pair of headers
* @return static
*/
public function setHeaders( $headers )
{
$this->headers = array_merge( $this->headers, $headers );
return $this;
}
/**
* Toggle SSL checks
*
* @param boolean $value True will enable SSL checks, false will disable them
* @return static
*/
public function sslCheck( $value=TRUE )
{
stream_context_set_option( $this->context, array(
'ssl' => array(
'verify_peer_name' => ( $value ) ? 2 : FALSE,
'verify_peer' => (boolean) $value,
)
) );
return $this;
}
/**
* Force TLS
*
* @return static
*/
public function forceTls()
{
if ( \defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT') )
{
stream_context_set_option( $this->context, array(
'ssl' => array(
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
)
) );
}
elseif ( \defined('STREAM_CRYPTO_METHOD_TLS_CLIENT') )
{
stream_context_set_option( $this->context, array(
'ssl' => array(
'crypto_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT
)
) );
}
return $this;
}
/**
* Magic Method: __call
* Used for other HTTP methods (like PUT and DELETE)
*
* @param string $method Method (A HTTP method)
* @param array $params Parameters (a single parameter with data to post, which can be an array or a string)
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\CurlException
*/
public function __call( $method, $params )
{
$method = mb_strtoupper( $method );
/* The data (string or array) will be the first parameter */
if ( isset( $params[0] ) && \is_array( $params[0] ) )
{
$this->setHeaders( array( 'Content-Type' => 'application/x-www-form-urlencoded' ) );
$data = http_build_query( $params[0], '', '&' );
}
else
{
$data = ( isset( $params[0] ) ? $params[0] : NULL );
}
/* Set the method and the Content-Length header if this is a POST, PUT or PATCH request */
stream_context_set_option( $this->context, 'http', 'method', $method );
if( $data )
{
$this->setHeaders( array( 'Content-Length' => \strlen( $data ) ) );
}
/* Parse URL */
if ( isset( $this->url->data['user'] ) or isset( $this->url->data['pass'] ) )
{
$this->login( isset( $this->url->data['user'] ) ? $this->url->data['user'] : NULL, isset( $this->url->data['pass'] ) ? $this->url->data['pass'] : NULL );
}
$hostname = sprintf( '%s%s:%d',
( $this->url->data['scheme'] === 'https' ) ? 'ssl://' : '',
$this->url->data['host'],
isset( $this->url->data['port'] )
? $this->url->data['port']
: ( $this->url->data['scheme'] === 'http' ? 80 : 443 )
);
/* Open connection */
try
{
$resource = stream_socket_client( $hostname, $errno, $errstr, $this->timeout, \STREAM_CLIENT_CONNECT, $this->context );
}
/* Catch issues that may arise, such as DNS failure */
catch( \ErrorException $e )
{
throw new SocketsException( $e->getMessage(), $e->getCode() );
}
if ( $resource === FALSE )
{
throw new SocketsException( $errstr, $errno );
}
/* Get the location */
$location = $this->url->data['path'] ? \IPS\Http\Url::encodeComponent( \IPS\Http\Url::COMPONENT_PATH, $this->url->data['path'] ) : '';
$location .= ( \count( $this->url->queryString ) ) ? '?' . \IPS\Http\Url::convertQueryAsArrayToString( $this->url->queryString, true ) : '';
$location .= $this->url->data['fragment'] ? '#' . \IPS\Http\Url::encodeComponent( \IPS\Http\Url::COMPONENT_FRAGMENT, $this->url->data['fragment'] ) : '';
/* Send request */
$request = mb_strtoupper( $method ) . ' /' . ltrim( $location, '/' ) . " HTTP/{$this->httpVersion}\r\n";
$request .= "Host: {$this->url->data['host']}" . ( isset( $this->url->data['port'] ) ? ":{$this->url->data['port']}" : '' ) . "\r\n";
$headersForLog = [];
foreach ( $this->headers as $k => $v )
{
$request .= "{$k}: {$v}\r\n";
$headersForLog[ $k ] = "{$k}: {$v}";
}
$request .= "Connection: Close\r\n";
$request .= "\r\n";
if ( $data )
{
$request .= $data;
}
\fwrite( $resource, $request );
/* Read response */
stream_set_timeout( $resource, $this->timeout );
$status = stream_get_meta_data( $resource );
$response = '';
while( !feof($resource) and !$status['timed_out'] )
{
$response .= \fgets( $resource, 8192 );
$status = stream_get_meta_data( $resource );
}
/* Close connection */
\fclose( $resource );
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
{
\IPS\Log::debug( "\n\n------------------------------------\nSOCKETS REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $headersForLog ) . "\n\n{$request}\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $response, 'request' );
}
/* Interpret response */
$response = new \IPS\Http\Response( $response );
/* Either return it or follow it */
if ( $this->followRedirects and \in_array( $response->httpResponseCode, array( 301, 302, 303, 307, 308 ) ) )
{
/* Fix missing hostname in location */
foreach( $response->httpHeaders as $k => $v )
{
if( mb_strtolower( $k ) == 'location' )
{
$location = $v;
}
}
if( parse_url( $location, PHP_URL_HOST ) === NULL )
{
$location = $this->url->data['scheme'] . '://' . $this->url->data['host'] . $location;
}
$newRequest = \IPS\Http\Url::external( $location );
if( !\in_array( $newRequest->data['scheme'], $this->allowedProtocols ) )
{
throw new \IPS\Http\Request\Exception( 'protocol_not_followed' );
}
$newRequest = $newRequest->request( $this->timeout, $this->httpVersion, \is_int( $this->followRedirects ) ? ( $this->followRedirects - 1 ) : $this->followRedirects );
return $newRequest->$method( $params );
}
return $response;
}
}
/**
* Sockets Exception Class
*/
class SocketsException extends \IPS\Http\Request\Exception { }
+41 -33
View File
@@ -11,54 +11,62 @@
namespace IPS\Http;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use InvalidArgumentException;
use IPS\Xml\SimpleXML;
use RuntimeException;
use function defined;
use function strpos;
use function substr;
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;
}
/**
* HTTP Response Class
*/
class _Response
class Response
{
/**
* @brief HTTP Response Version
*/
public $httpResponseVersion = NULL;
public mixed $httpResponseVersion = NULL;
/**
* @brief HTTP Response Code
*/
public $httpResponseCode = NULL;
public mixed $httpResponseCode = NULL;
/**
* @brief HTTP Response Text
*/
public $httpResponseText = NULL;
public mixed $httpResponseText = NULL;
/**
* @brief HTTP Headers
*/
public $httpHeaders = NULL;
public ?array $httpHeaders = NULL;
/**
* @brief Cookies returned in the response
*/
public $cookies = array();
public array $cookies = array();
/**
* @brief Response Content
*/
public $content = '';
public mixed $content = '';
/**
* Constructor
*
* @param string $content Response Content
* @param string $content Response Content
* @return void
*/
public function __construct( $content )
public function __construct( string $content )
{
/* Do we have any content? */
if( !$content )
@@ -169,19 +177,19 @@ class _Response
/**
* Decode chunked response body from HTTP 1.1 response
*
* @see <a href='http://stackoverflow.com/questions/10793017/how-to-easily-decode-http-chunked-encoded-string-when-making-raw-http-request'>Decoding chunked response in PHP</a>
* @param string $body Response body
* @return string
* @note We are working with bytes and intentionally are not using mb* functions
*@see <a href='http://stackoverflow.com/questions/10793017/how-to-easily-decode-http-chunked-encoded-string-when-making-raw-http-request'>Decoding chunked response in PHP</a>
*/
public function decodeChunked( $body )
public function decodeChunked( string $body ): string
{
$response = '';
$initial = $body;
for( $response = ''; !empty($body); $body = trim($body) )
{
$_pos = \strpos( $body, "\r\n" );
$_pos = strpos( $body, "\r\n" );
/* The last line could be 0000000 which is valid but doesn't have \r\n */
if( trim( $body, '0' ) === '' )
@@ -190,20 +198,20 @@ class _Response
}
/* Make sure this is valid hex */
if( $_pos === FALSE OR !preg_match( "/^([a-f0-9]+)$/mi", \substr( $body, 0, $_pos ) ) )
if( $_pos === FALSE OR !preg_match( "/^([a-f0-9]+)$/mi", substr( $body, 0, $_pos ) ) )
{
return $response ?: $initial;
}
$_len = (int) hexdec( \substr( $body, 0, $_pos ) );
$_len = (int) hexdec( substr( $body, 0, $_pos ) );
if( $_len < 1 )
{
return $initial;
}
$response .= \substr( $body, $_pos + 2, $_len );
$body = \substr( $body, $_pos + 2 + $_len );
$response .= substr( $body, $_pos + 2, $_len );
$body = substr( $body, $_pos + 2 + $_len );
$body = trim($body);
}
@@ -223,17 +231,17 @@ class _Response
/**
* Decode JSON
*
* @param bool $asArray Whether to decode as an array or not
* @param bool $asArray Whether to decode as an array or not
* @return array
* @throws \RuntimeException
* @throws RuntimeException
* @li BAD_JSON
*/
public function decodeJson( $asArray=TRUE )
public function decodeJson( bool $asArray=TRUE ): mixed
{
$json = json_decode( $this->content, $asArray );
if ( $json === FALSE )
if ( $json === FALSE or $json === NULL )
{
throw new \RuntimeException('BAD_JSON');
throw new RuntimeException('BAD_JSON');
}
return $json;
}
@@ -241,19 +249,19 @@ class _Response
/**
* Decode XML
*
* @return \IPS\Xml\SimpleXML
* @throws \RuntimeException
* @return SimpleXML
* @throws RuntimeException
* @li BAD_XML
*/
public function decodeXml()
public function decodeXml(): SimpleXML
{
try
{
$xml = \IPS\Xml\SimpleXML::loadString( $this->content );
$xml = SimpleXML::loadString( $this->content );
}
catch ( \InvalidArgumentException $e )
catch ( InvalidArgumentException $e )
{
throw new \RuntimeException('BAD_XML');
throw new RuntimeException('BAD_XML');
}
return $xml;
@@ -262,18 +270,18 @@ class _Response
/**
* Decode Query String
*
* @param string $expectedKey If provided, the return value will check for an element with this key and if it isn't present, throw an Exception
* @param string|null $expectedKey If provided, the return value will check for an element with this key and if it isn't present, throw an Exception
* @return array
* @throws \RuntimeException
* @throws RuntimeException
* @li EXPECTED_KEY_NOT_PRESENT
*/
public function decodeQueryString( $expectedKey=NULL )
public function decodeQueryString( string $expectedKey=NULL ): array
{
@parse_str( $this->content, $queryString );
if ( $expectedKey !== NULL and !array_key_exists( $expectedKey, $queryString ) )
{
throw new \RuntimeException( 'EXPECTED_KEY_NOT_PRESENT' );
throw new RuntimeException( 'EXPECTED_KEY_NOT_PRESENT' );
}
return $queryString;
+466 -206
View File
File diff suppressed because it is too large. Load diff
+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}";
}
}
+74 -102
View File
@@ -11,68 +11,61 @@
namespace IPS\Http;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Member;
use function defined;
use function is_array;
use function ord;
use function strpos;
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;
}
/**
* User-Agent Management Class
*/
class _Useragent
class Useragent
{
CONST TYPE_SEARCH = 1;
CONST TYPE_AI = 2;
CONST TYPE_CRAWLER = 3;
CONST TYPE_OTHER = 4;
/**
* @brief [Legacy] Search engine spider?
* @brief Search engine spider?
*/
public bool $spider = FALSE;
/**
* @brief Bot?
*/
public bool $bot = FALSE;
/**
* @brief Bot type
*/
public int $type = 0;
/**
* @brief Browser name
*/
public $browser = NULL;
public ?string $browser = NULL;
/**
* @brief Browser version
*/
public $browserVersion = NULL;
public ?string $browserVersion = NULL;
/**
* @brief Platform Name
*/
public $platform = NULL;
public ?string $platform = NULL;
/**
* @brief Full user agent string
*/
public $useragent = NULL;
public ?string $useragent = NULL;
/**
* @brief Store parsed agents
*/
protected static $parsedAgents = array();
protected static array $parsedAgents = array();
/**
* Constructor
*
* @param string $userAgent The user agent string
* @param string $userAgent The user agent string
* @return void
*/
protected function __construct( $userAgent )
protected function __construct( string $userAgent )
{
$this->useragent = $userAgent;
}
@@ -80,10 +73,10 @@ class _Useragent
/**
* Constructor
*
* @param string $userAgent The user agent to parse (defaults to $_SERVER['HTTP_USER_AGENT'] if none supplied)
* @return \IPS\Http\Useragent
* @param string|null $userAgent The user agent to parse (defaults to $_SERVER['HTTP_USER_AGENT'] if none supplied)
* @return Useragent
*/
public static function parse( $userAgent=NULL )
public static function parse( string $userAgent=NULL ): Useragent
{
$userAgent = $userAgent ?: ( ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) ? $_SERVER['HTTP_USER_AGENT'] : '' );
@@ -103,7 +96,7 @@ class _Useragent
*
* @return void
*/
public function parseUserAgent()
public function parseUserAgent() : void
{
/* Set basic data */
require_once( \IPS\ROOT_PATH . '/system/3rd_party/PhpUserAgent/UserAgentParser.php' );
@@ -113,34 +106,30 @@ class _Useragent
$this->browserVersion = $data['version'];
/* Is this a spider? */
foreach( $this->searchEngineUseragents as $key => $data )
foreach( $this->searchEngineUseragents as $key => $regex )
{
if( \is_array( $data[1] ) )
if( is_array( $regex ) )
{
foreach( $data[1] as $_expression )
foreach( $regex as $_expression )
{
if( preg_match( "#" . $_expression . "#im", $this->useragent, $matches ) )
{
$this->spider = $key; // keep this for older apps
$this->bot = $key;
$this->type = $data[0];
$this->spider = $key;
break 2;
}
}
}
else
{
if( preg_match( "#" . $data[1] . "#im", $this->useragent, $matches ) )
if( preg_match( "#" . $regex . "#im", $this->useragent, $matches ) )
{
$this->spider = $key; // Keep this for older apps
$this->bot = $key;
$this->type = $data[0];
$this->spider = $key;
break;
}
}
}
}
/**
* @brief List of search engine user agent strings with regex to parse out the data.
* @note Matches will be checked based on the order of this list - put more specific matches first and more generic matches later
@@ -148,46 +137,39 @@ class _Useragent
* @note You may put multiple regex definitions for a single key into an array
* @note If the UA matches an entry in this array, $this->spider will be set to TRUE
*/
protected $searchEngineUseragents = array(
'about' => [ self::TYPE_OTHER, "Libby[_/ ]([0-9.]{1,10})" ],
'adsense' => [ self::TYPE_OTHER, array( "Mediapartners-Google/([0-9.]{1,10})", "Mediapartners-Google" ) ],
'ahrefs' => [ self::TYPE_CRAWLER, "AhrefsBot" ],
'alexa' => [ self::TYPE_SEARCH, "^ia_archive" ],
'altavista' => [ self::TYPE_OTHER, "Scooter[ /\-]*[a-z]*([0-9.]{1,10})" ],
'amazon' => [ self::TYPE_AI, [ "developer\.amazon\.com", "amazonbot" ] ],
'ask' => [ self::TYPE_OTHER, "Ask[ \-]?Jeeves" ],
'baidu' => [ self::TYPE_OTHER, array( "^baiduspider\-", "baiduspider[ /]([0-9.]{1,10})" ) ],
'bing' => [ self::TYPE_SEARCH, array( "bingbot[ /]([0-9.]{1,10})", "msnbot(?:-media)?[ /]([0-9.]{1,10})" ) ],
'brandwatch' => [ self::TYPE_OTHER, "magpie-crawler" ],
'chatgpt' => [ self::TYPE_AI, [ "OAI-SearchBot/([0-9.]{1,10})", "ChatGPT-User/([0-9.]{1,10})", "GPTBot/([0-9.]{1,10})", "openai.com/"] ],
'commoncrawl' => [ self::TYPE_OTHER, "CCBot" ],
'excite' => [ self::TYPE_OTHER, "Architext[ \-]?Spider" ],
'google' => [ self::TYPE_SEARCH, array( "Googl(?:e|ebot)(?:-Image|-Video|-News)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Image|-Video|-News)?/?" ) ],
'googlemobile' => [ self::TYPE_SEARCH, array( "Googl(?:e|ebot)(?:-Mobile)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Mobile)?/" ) ],
'facebook' => [ self::TYPE_OTHER, "facebookexternalhit/([0-9.]{1,10})" ],
'infoseek' => [ self::TYPE_OTHER, array( "SideWinder[ /]?([0-9a-z.]{1,10})", "Infoseek" ) ],
'inktomi' => [ self::TYPE_OTHER, "slurp@inktomi\.com" ],
'internetseer' => [ self::TYPE_SEARCH, "^InternetSeer\.com" ],
'look' => [ self::TYPE_OTHER, "www\.look\.com" ],
'looksmart' => [ self::TYPE_OTHER, "looksmart-sv-fw" ],
'lycos' => [ self::TYPE_OTHER, "Lycos_Spider_" ],
'majestic' => [ self::TYPE_OTHER, "MJ12bot\/v([0-9.]{1,10})" ],
'meta' => [ self::TYPE_OTHER, [ "meta-externalagent/", "developers\.facebook\.com" ] ],
'msproxy' => [ self::TYPE_OTHER, "MSProxy[ /]([0-9.]{1,10})" ],
'perplexity' => [ self::TYPE_OTHER, "PerplexityBot" ],
'pinterest' => [ self::TYPE_OTHER, "Pinterestbot/([0-9.]{1,10})" ],
'webcrawl' => [ self::TYPE_CRAWLER, "webcrawl\.net" ],
'websense' => [ self::TYPE_OTHER, "(?:Sqworm|websense|Konqueror/3\.(?:0|1)(?:\-rc[1-6])?; i686 Linux; 2002[0-9]{4})" ],
'yahoo' => [ self::TYPE_SEARCH, "Yahoo(?:.*?)(?:Slurp|FeedSeeker)" ],
'yandex' => [ self::TYPE_OTHER, "Yandex(?:[^\/]+?)\/([0-9.]{1,10})" ],
'seznam' => [ self::TYPE_OTHER, array( "SeznamBot[ /]([0-9.]{1,10})", "Seznam screenshot-generator ([0-9.]{1,10})" ) ],
'dotbot' => [ self::TYPE_OTHER, "DotBot[ /]([0-9.]{1,10})" ],
'sogou' => [ self::TYPE_OTHER, "Sogou web spider[ /]([0-9.]{1,10})" ],
'isetallabot' => [ self::TYPE_OTHER, "istellabot[ /][a-z]([0-9.]{1,10})" ],
'blexbot' => [ self::TYPE_OTHER, "BLEXBot[ /]([0-9.]{1,10})" ],
'semrush' => [ self::TYPE_CRAWLER, "SemrushBot/([0-9.]{1,10})" ]
protected array $searchEngineUseragents = array(
'about' => "Libby[_/ ]([0-9.]{1,10})",
'adsense' => array( "Mediapartners-Google/([0-9.]{1,10})", "Mediapartners-Google" ),
'ahrefs' => "AhrefsBot",
'alexa' => "^ia_archive",
'altavista' => "Scooter[ /\-]*[a-z]*([0-9.]{1,10})",
'ask' => "Ask[ \-]?Jeeves",
'baidu' => array( "^baiduspider\-", "baiduspider[ /]([0-9.]{1,10})" ),
'bing' => array( "bingbot[ /]([0-9.]{1,10})", "msnbot(?:-media)?[ /]([0-9.]{1,10})" ),
'brandwatch' => "magpie-crawler",
'excite' => "Architext[ \-]?Spider",
'google' => array( "Googl(?:e|ebot)(?:-Image|-Video|-News)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Image|-Video|-News)?/?" ),
'googlemobile' => array( "Googl(?:e|ebot)(?:-Mobile)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Mobile)?/" ),
'facebook' => "facebookexternalhit/([0-9.]{1,10})",
'infoseek' => array( "SideWinder[ /]?([0-9a-z.]{1,10})", "Infoseek" ),
'inktomi' => "slurp@inktomi\.com",
'internetseer' => "^InternetSeer\.com",
'look' => "www\.look\.com",
'looksmart' => "looksmart-sv-fw",
'lycos' => "Lycos_Spider_",
'majestic' => "MJ12bot\/v([0-9.]{1,10})",
'msproxy' => "MSProxy[ /]([0-9.]{1,10})",
'webcrawl' => "webcrawl\.net",
'websense' => "(?:Sqworm|websense|Konqueror/3\.(?:0|1)(?:\-rc[1-6])?; i686 Linux; 2002[0-9]{4})",
'yahoo' => "Yahoo(?:.*?)(?:Slurp|FeedSeeker)",
'yandex' => "Yandex(?:[^\/]+?)\/([0-9.]{1,10})",
'seznam' => array( "SeznamBot[ /]([0-9.]{1,10})", "Seznam screenshot-generator ([0-9.]{1,10})" ),
'dotbot' => "DotBot[ /]([0-9.]{1,10})",
'sogou' => "Sogou web spider[ /]([0-9.]{1,10})",
'isetallabot' => "istellabot[ /][a-z]([0-9.]{1,10})",
'blexbot' => "BLEXBot[ /]([0-9.]{1,10})",
'semrush' => "SemrushBot/([0-9.]{1,10})"
);
/**
* Human-Readable Browser Name
@@ -196,17 +178,7 @@ class _Useragent
*/
public function __toString()
{
return \IPS\Member::loggedIn()->language()->addToStack( 'user_agent_parsed', FALSE, array( 'sprintf' => array( $this->browser, $this->browserVersion, $this->platform ) ) );
}
/**
* Is this a low value bot?
*
* @return bool
*/
public function isLowValue(): bool
{
return $this->bot === true and $this->type !== self::TYPE_SEARCH;
return Member::loggedIn()->language()->addToStack( 'user_agent_parsed', FALSE, array( 'sprintf' => array( $this->browser, $this->browserVersion, $this->platform ) ) );
}
/**
@@ -214,31 +186,31 @@ class _Useragent
* @see <a href='https://developers.facebook.com/docs/ApplicationSecurity/#facebook_scraper'>Facebook application security</a>
* @note List pulled via suggested whois command on Dec 13 2016
*/
protected $facebookIpRange = array('204.15.20.0/22', '69.63.176.0/20', '66.220.144.0/20', '66.220.144.0/21', '69.63.184.0/21', '69.63.176.0/21', '74.119.76.0/22', '69.171.255.0/24', '173.252.64.0/18', '69.171.224.0/19', '69.171.224.0/20', '103.4.96.0/22', '69.63.176.0/24', '173.252.64.0/19', '173.252.70.0/24', '31.13.64.0/18', '31.13.24.0/21', '66.220.152.0/21', '66.220.159.0/24', '69.171.239.0/24', '69.171.240.0/20', '31.13.64.0/19', '31.13.64.0/24', '31.13.65.0/24', '31.13.67.0/24', '31.13.68.0/24', '31.13.69.0/24', '31.13.70.0/24', '31.13.71.0/24', '31.13.72.0/24', '31.13.73.0/24', '31.13.74.0/24', '31.13.75.0/24', '31.13.76.0/24', '31.13.77.0/24', '31.13.96.0/19', '31.13.66.0/24', '173.252.96.0/19', '69.63.178.0/24', '31.13.78.0/24', '31.13.79.0/24', '31.13.80.0/24', '31.13.82.0/24', '31.13.83.0/24', '31.13.84.0/24', '31.13.85.0/24', '31.13.86.0/24', '31.13.87.0/24', '31.13.88.0/24', '31.13.89.0/24', '31.13.90.0/24', '31.13.91.0/24', '31.13.92.0/24', '31.13.93.0/24', '31.13.94.0/24', '31.13.95.0/24', '69.171.253.0/24', '69.63.186.0/24', '31.13.81.0/24', '179.60.192.0/22', '179.60.192.0/24', '179.60.193.0/24', '179.60.194.0/24', '179.60.195.0/24', '185.60.216.0/22', '45.64.40.0/22', '185.60.216.0/24', '185.60.217.0/24', '185.60.218.0/24', '185.60.219.0/24', '129.134.0.0/16', '157.240.0.0/16', '157.240.8.0/24', '157.240.0.0/24', '157.240.1.0/24', '157.240.2.0/24', '157.240.3.0/24', '157.240.4.0/24', '157.240.5.0/24', '157.240.6.0/24', '157.240.7.0/24', '157.240.9.0/24', '157.240.10.0/24', '204.15.20.0/22', '69.63.176.0/20', '69.63.176.0/21', '69.63.184.0/21', '66.220.144.0/20', ' 69.63.176.0/20', '2620:0:1c00::/40', '2a03:2880::/32', '2a03:2880:fffe::/48', '2a03:2880:ffff::/48', '2620:0:1cff::/48', '2a03:2880:f000::/48', '2a03:2880:f001::/48', '2a03:2880:f002::/48', '2a03:2880:f003::/48', '2a03:2880:f004::/48', '2a03:2880:f005::/48', '2a03:2880:f006::/48', '2a03:2880:f007::/48', '2a03:2880:f008::/48', '2a03:2880:f009::/48', '2a03:2880:f00a::/48', '2a03:2880:f00b::/48', '2a03:2880:f00c::/48', '2a03:2880:f00d::/48', '2a03:2880:f00e::/48', '2a03:2880:f00f::/48', '2a03:2880:f010::/48', '2a03:2880:f011::/48', '2a03:2880:f012::/48', '2a03:2880:f013::/48', '2a03:2880:f014::/48', '2a03:2880:f015::/48', '2a03:2880:f016::/48', '2a03:2880:f017::/48', '2a03:2880:f018::/48', '2a03:2880:f019::/48', '2a03:2880:f01a::/48', '2a03:2880:f01b::/48', '2a03:2880:f01c::/48', '2a03:2880:f01d::/48', '2a03:2880:f01e::/48', '2a03:2880:f01f::/48', '2a03:2880:1000::/36', '2a03:2880:2000::/36', '2a03:2880:3000::/36', '2a03:2880:4000::/36', '2a03:2880:5000::/36', '2a03:2880:6000::/36', '2a03:2880:7000::/36', '2a03:2880:f020::/48', '2a03:2880:f021::/48', '2a03:2880:f022::/48', '2a03:2880:f023::/48', '2a03:2880:f024::/48', '2a03:2880:f025::/48', '2a03:2880:f026::/48', '2a03:2880:f027::/48', '2a03:2880:f028::/48', '2a03:2880:f029::/48', '2a03:2880:f02b::/48', '2a03:2880:f02c::/48', '2a03:2880:f02d::/48', '2a03:2880:f02e::/48', '2a03:2880:f02f::/48', '2a03:2880:f030::/48', '2a03:2880:f031::/48', '2a03:2880:f032::/48', '2a03:2880:f033::/48', '2a03:2880:f034::/48', '2a03:2880:f035::/48', '2a03:2880:f036::/48', '2a03:2880:f037::/48', '2a03:2880:f038::/48', '2a03:2880:f039::/48', '2a03:2880:f03a::/48', '2a03:2880:f03b::/48', '2a03:2880:f03c::/48', '2a03:2880:f03d::/48', '2a03:2880:f03e::/48', '2a03:2880:f03f::/48', '2401:db00::/32', '2a03:2880::/36', '2803:6080::/32', '2a03:2880:f100::/48', '2a03:2880:f200::/48', '2a03:2880:f101::/48', '2a03:2880:f201::/48', '2a03:2880:f102::/48', '2a03:2880:f202::/48', '2a03:2880:f103::/48', '2a03:2880:f203::/48', '2a03:2880:f104::/48', '2a03:2880:f204::/48', '2a03:2880:f107::/48', '2a03:2880:f207::/48', '2a03:2880:f108::/48', '2a03:2880:f208::/48', '2a03:2880:f109::/48', '2a03:2880:f209::/48', '2a03:2880:f10a::/48', '2a03:2880:f20a::/48', '2a03:2880:f10b::/48', '2a03:2880:f20b::/48', '2a03:2880:f10d::/48', '2a03:2880:f20d::/48', '2a03:2880:f10e::/48', '2a03:2880:f20e::/48', '2a03:2880:f10f::/48', '2a03:2880:f20f::/48', '2a03:2880:f110::/48', '2a03:2880:f210::/48', '2a03:2880:f111::/48', '2a03:2880:f211::/48', '2a03:2880:f112::/48', '2a03:2880:f212::/48', '2a03:2880:f114::/48', '2a03:2880:f214::/48', '2a03:2880:f115::/48', '2a03:2880:f215::/48', '2a03:2880:f116::/48', '2a03:2880:f216::/48', '2a03:2880:f117::/48', '2a03:2880:f217::/48', '2a03:2880:f118::/48', '2a03:2880:f218::/48', '2a03:2880:f119::/48', '2a03:2880:f219::/48', '2a03:2880:f11a::/48', '2a03:2880:f21a::/48', '2a03:2880:f11f::/48', '2a03:2880:f21f::/48', '2a03:2880:f121::/48', '2a03:2880:f221::/48', '2a03:2880:f122::/48', '2a03:2880:f222::/48', '2a03:2880:f123::/48', '2a03:2880:f223::/48', '2a03:2880:f10c::/48', '2a03:2880:f20c::/48', '2a03:2880:f126::/48', '2a03:2880:f226::/48', '2a03:2880:f105::/48', '2a03:2880:f205::/48', '2a03:2880:f125::/48', '2a03:2880:f225::/48', '2a03:2880:f106::/48', '2a03:2880:f206::/48', '2a03:2880:f11b::/48', '2a03:2880:f21b::/48', '2a03:2880:f113::/48', '2a03:2880:f213::/48', '2a03:2880:f11c::/48', '2a03:2880:f21Line truncated
protected array $facebookIpRange = array('204.15.20.0/22', '69.63.176.0/20', '66.220.144.0/20', '66.220.144.0/21', '69.63.184.0/21', '69.63.176.0/21', '74.119.76.0/22', '69.171.255.0/24', '173.252.64.0/18', '69.171.224.0/19', '69.171.224.0/20', '103.4.96.0/22', '69.63.176.0/24', '173.252.64.0/19', '173.252.70.0/24', '31.13.64.0/18', '31.13.24.0/21', '66.220.152.0/21', '66.220.159.0/24', '69.171.239.0/24', '69.171.240.0/20', '31.13.64.0/19', '31.13.64.0/24', '31.13.65.0/24', '31.13.67.0/24', '31.13.68.0/24', '31.13.69.0/24', '31.13.70.0/24', '31.13.71.0/24', '31.13.72.0/24', '31.13.73.0/24', '31.13.74.0/24', '31.13.75.0/24', '31.13.76.0/24', '31.13.77.0/24', '31.13.96.0/19', '31.13.66.0/24', '173.252.96.0/19', '69.63.178.0/24', '31.13.78.0/24', '31.13.79.0/24', '31.13.80.0/24', '31.13.82.0/24', '31.13.83.0/24', '31.13.84.0/24', '31.13.85.0/24', '31.13.86.0/24', '31.13.87.0/24', '31.13.88.0/24', '31.13.89.0/24', '31.13.90.0/24', '31.13.91.0/24', '31.13.92.0/24', '31.13.93.0/24', '31.13.94.0/24', '31.13.95.0/24', '69.171.253.0/24', '69.63.186.0/24', '31.13.81.0/24', '179.60.192.0/22', '179.60.192.0/24', '179.60.193.0/24', '179.60.194.0/24', '179.60.195.0/24', '185.60.216.0/22', '45.64.40.0/22', '185.60.216.0/24', '185.60.217.0/24', '185.60.218.0/24', '185.60.219.0/24', '129.134.0.0/16', '157.240.0.0/16', '157.240.8.0/24', '157.240.0.0/24', '157.240.1.0/24', '157.240.2.0/24', '157.240.3.0/24', '157.240.4.0/24', '157.240.5.0/24', '157.240.6.0/24', '157.240.7.0/24', '157.240.9.0/24', '157.240.10.0/24', '204.15.20.0/22', '69.63.176.0/20', '69.63.176.0/21', '69.63.184.0/21', '66.220.144.0/20', ' 69.63.176.0/20', '2620:0:1c00::/40', '2a03:2880::/32', '2a03:2880:fffe::/48', '2a03:2880:ffff::/48', '2620:0:1cff::/48', '2a03:2880:f000::/48', '2a03:2880:f001::/48', '2a03:2880:f002::/48', '2a03:2880:f003::/48', '2a03:2880:f004::/48', '2a03:2880:f005::/48', '2a03:2880:f006::/48', '2a03:2880:f007::/48', '2a03:2880:f008::/48', '2a03:2880:f009::/48', '2a03:2880:f00a::/48', '2a03:2880:f00b::/48', '2a03:2880:f00c::/48', '2a03:2880:f00d::/48', '2a03:2880:f00e::/48', '2a03:2880:f00f::/48', '2a03:2880:f010::/48', '2a03:2880:f011::/48', '2a03:2880:f012::/48', '2a03:2880:f013::/48', '2a03:2880:f014::/48', '2a03:2880:f015::/48', '2a03:2880:f016::/48', '2a03:2880:f017::/48', '2a03:2880:f018::/48', '2a03:2880:f019::/48', '2a03:2880:f01a::/48', '2a03:2880:f01b::/48', '2a03:2880:f01c::/48', '2a03:2880:f01d::/48', '2a03:2880:f01e::/48', '2a03:2880:f01f::/48', '2a03:2880:1000::/36', '2a03:2880:2000::/36', '2a03:2880:3000::/36', '2a03:2880:4000::/36', '2a03:2880:5000::/36', '2a03:2880:6000::/36', '2a03:2880:7000::/36', '2a03:2880:f020::/48', '2a03:2880:f021::/48', '2a03:2880:f022::/48', '2a03:2880:f023::/48', '2a03:2880:f024::/48', '2a03:2880:f025::/48', '2a03:2880:f026::/48', '2a03:2880:f027::/48', '2a03:2880:f028::/48', '2a03:2880:f029::/48', '2a03:2880:f02b::/48', '2a03:2880:f02c::/48', '2a03:2880:f02d::/48', '2a03:2880:f02e::/48', '2a03:2880:f02f::/48', '2a03:2880:f030::/48', '2a03:2880:f031::/48', '2a03:2880:f032::/48', '2a03:2880:f033::/48', '2a03:2880:f034::/48', '2a03:2880:f035::/48', '2a03:2880:f036::/48', '2a03:2880:f037::/48', '2a03:2880:f038::/48', '2a03:2880:f039::/48', '2a03:2880:f03a::/48', '2a03:2880:f03b::/48', '2a03:2880:f03c::/48', '2a03:2880:f03d::/48', '2a03:2880:f03e::/48', '2a03:2880:f03f::/48', '2401:db00::/32', '2a03:2880::/36', '2803:6080::/32', '2a03:2880:f100::/48', '2a03:2880:f200::/48', '2a03:2880:f101::/48', '2a03:2880:f201::/48', '2a03:2880:f102::/48', '2a03:2880:f202::/48', '2a03:2880:f103::/48', '2a03:2880:f203::/48', '2a03:2880:f104::/48', '2a03:2880:f204::/48', '2a03:2880:f107::/48', '2a03:2880:f207::/48', '2a03:2880:f108::/48', '2a03:2880:f208::/48', '2a03:2880:f109::/48', '2a03:2880:f209::/48', '2a03:2880:f10a::/48', '2a03:2880:f20a::/48', '2a03:2880:f10b::/48', '2a03:2880:f20b::/48', '2a03:2880:f10d::/48', '2a03:2880:f20d::/48', '2a03:2880:f10e::/48', '2a03:2880:f20e::/48', '2a03:2880:f10f::/48', '2a03:2880:f20f::/48', '2a03:2880:f110::/48', '2a03:2880:f210::/48', '2a03:2880:f111::/48', '2a03:2880:f211::/48', '2a03:2880:f112::/48', '2a03:2880:f212::/48', '2a03:2880:f114::/48', '2a03:2880:f214::/48', '2a03:2880:f115::/48', '2a03:2880:f215::/48', '2a03:2880:f116::/48', '2a03:2880:f216::/48', '2a03:2880:f117::/48', '2a03:2880:f217::/48', '2a03:2880:f118::/48', '2a03:2880:f218::/48', '2a03:2880:f119::/48', '2a03:2880:f219::/48', '2a03:2880:f11a::/48', '2a03:2880:f21a::/48', '2a03:2880:f11f::/48', '2a03:2880:f21f::/48', '2a03:2880:f121::/48', '2a03:2880:f221::/48', '2a03:2880:f122::/48', '2a03:2880:f222::/48', '2a03:2880:f123::/48', '2a03:2880:f223::/48', '2a03:2880:f10c::/48', '2a03:2880:f20c::/48', '2a03:2880:f126::/48', '2a03:2880:f226::/48', '2a03:2880:f105::/48', '2a03:2880:f205::/48', '2a03:2880:f125::/48', '2a03:2880:f225::/48', '2a03:2880:f106::/48', '2a03:2880:f206::/48', '2a03:2880:f11b::/48', '2a03:2880:f21b::/48', '2a03:2880:f113::/48', '2a03:2880:f213::/48', '2a03:2880:f11c::/48', '2a03:28Line truncated
/**
* Verify a supplied IP address is within the Facebook range
*
* @param string $ip IP address to check
* @param string $ip IP address to check
* @return bool
* @see <a href='http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet'>Stackoverflow: check IPv6 against CIDR</a>
* @see <a href='http://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php5'>Stackoverflow: check IPv4 against CIDR</a>
*/
public function facebookIpVerified( $ip )
public function facebookIpVerified( string $ip ): bool
{
/* Is this an IPv6 address? */
if( \strpos( $ip, ':' ) !== FALSE )
if( strpos( $ip, ':' ) !== FALSE )
{
$ip = $this->_convertCompressedIpv6ToBits( inet_pton( $ip ) );
foreach( $this->facebookIpRange as $range )
{
if( \strpos( $range, ':' ) === FALSE )
if( strpos( $range, ':' ) === FALSE )
{
continue;
}
[ $net, $maskBits ] = explode( '/', $range );
list( $net, $maskBits ) = explode( '/', $range );
$net = $this->_convertCompressedIpv6ToBits( inet_pton( $net ) );
@@ -252,12 +224,12 @@ class _Useragent
{
foreach( $this->facebookIpRange as $range )
{
if( \strpos( $range, ':' ) !== FALSE )
if( strpos( $range, ':' ) !== FALSE )
{
continue;
}
[ $net, $maskBits ] = explode( '/', $range );
list( $net, $maskBits ) = explode( '/', $range );
if( ( ip2long( $ip ) & ~( ( 1 << ( 32 - $maskBits ) ) - 1 ) ) == ip2long( $net ) )
{
@@ -272,11 +244,11 @@ class _Useragent
/**
* Convert an IPv6 address to bits
*
* @param string $ip Compressed IPv6 address
* @return array
* @param string $ip Compressed IPv6 address
* @return array|string
* @see <a href='http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet'>Stackoverflow: check IPv6 against CIDR</a>
*/
protected function _convertCompressedIpv6ToBits( $ip )
protected function _convertCompressedIpv6ToBits( string $ip ): array|string
{
$unpackedAddress = unpack( 'A16', $ip );
$unpackedAddress = str_split( $unpackedAddress[1] );
@@ -284,7 +256,7 @@ class _Useragent
foreach( $unpackedAddress as $char )
{
$ipAddress .= str_pad( decbin( \ord( $char ) ), 8, '0', STR_PAD_LEFT );
$ipAddress .= str_pad( decbin( ord( $char ) ), 8, '0', STR_PAD_LEFT );
}
return $ipAddress;