Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
+75
-67
@@ -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
@@ -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
|
||||
{ }
|
||||
@@ -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 { }
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user