269 lines
6.2 KiB
PHP
269 lines
6.2 KiB
PHP
<?php
|
|
/**
|
|
* @brief HTTP Response 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;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/**
|
|
* HTTP Response Class
|
|
*/
|
|
class _Response
|
|
{
|
|
/**
|
|
* @brief HTTP Response Version
|
|
*/
|
|
public $httpResponseVersion = NULL;
|
|
|
|
/**
|
|
* @brief HTTP Response Code
|
|
*/
|
|
public $httpResponseCode = NULL;
|
|
|
|
/**
|
|
* @brief HTTP Response Text
|
|
*/
|
|
public $httpResponseText = NULL;
|
|
|
|
/**
|
|
* @brief HTTP Headers
|
|
*/
|
|
public $httpHeaders = NULL;
|
|
|
|
/**
|
|
* @brief Cookies returned in the response
|
|
*/
|
|
public $cookies = array();
|
|
|
|
/**
|
|
* @brief Response Content
|
|
*/
|
|
public $content = '';
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string $content Response Content
|
|
* @return void
|
|
*/
|
|
public function __construct( $content )
|
|
{
|
|
/* Do we have any content? */
|
|
if( !$content )
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Extract the HTTP response information */
|
|
do
|
|
{
|
|
if( !$content )
|
|
{
|
|
break;
|
|
}
|
|
|
|
$firstLineBreak = mb_strpos( $content, "\n" );
|
|
if ( preg_match( "/^HTTP\/(.+?) (\d+?) (.+?)$/", mb_substr( $content, 0, $firstLineBreak ), $matches ) )
|
|
{
|
|
$this->httpResponseVersion = $matches[1];
|
|
$this->httpResponseCode = $matches[2];
|
|
$this->httpResponseText = $matches[3];
|
|
}
|
|
|
|
/* If we have received a 1xx response code, that means it's not the final response */
|
|
if ( mb_substr( $this->httpResponseCode, 0, 1 ) == 1 OR ( $this->httpResponseCode == 200 AND trim( $this->httpResponseText ) == 'Connection established' ) )
|
|
{
|
|
$content = trim( mb_substr( $content, $firstLineBreak ) );
|
|
}
|
|
/* Or a 3xx response with another response */
|
|
elseif ( mb_substr( $this->httpResponseCode, 0, 1 ) == 3 AND mb_strpos( mb_substr( $content, $firstLineBreak ), "HTTP/" ) !== FALSE )
|
|
{
|
|
$_content = trim( mb_substr( $content, $firstLineBreak ) );
|
|
if ( $_content )
|
|
{
|
|
$content = $_content;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
while ( TRUE );
|
|
|
|
/* Split into headers and content */
|
|
$split = preg_split( "/\r\n\r\n/", mb_substr( $content, $firstLineBreak ), 2 );
|
|
if ( isset( $split[1] ) )
|
|
{
|
|
$this->content = $split[1];
|
|
}
|
|
foreach ( explode( "\n", $split[0] ) as $line )
|
|
{
|
|
$line = trim( $line );
|
|
|
|
if ( $line !== '' and preg_match( "/^(.+?): (.+?)$/", $line, $matches ) )
|
|
{
|
|
$this->httpHeaders[ $matches[1] ] = $matches[2];
|
|
|
|
if( mb_strtolower( $matches[1] ) == 'set-cookie' )
|
|
{
|
|
$cookie = array();
|
|
$cookieDetails = explode( ';', $matches[2] );
|
|
|
|
$cookieInfo = array_shift( $cookieDetails );
|
|
$cookieInfo = explode( '=', $cookieInfo );
|
|
|
|
$cookie[ $cookieInfo[0] ] = array( 'value' => rawurldecode( $cookieInfo[1] ) );
|
|
|
|
foreach( $cookieDetails as $cookieDetail )
|
|
{
|
|
$cookieDetail = explode( '=', $cookieDetail );
|
|
|
|
$cookie[ $cookieInfo[0] ][ trim( $cookieDetail[0] ) ] = ( isset( $cookieDetail[1] ) ) ? $cookieDetail[1] : true;
|
|
}
|
|
|
|
$this->cookies = array_merge( $this->cookies, $cookie );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Fix chunked encoding */
|
|
if( isset( $this->httpHeaders['transfer-encoding'] ) )
|
|
{
|
|
$this->httpHeaders['Transfer-Encoding'] = $this->httpHeaders['transfer-encoding'];
|
|
}
|
|
|
|
if( isset( $this->httpHeaders['Transfer-Encoding'] ) and mb_strtolower( trim( $this->httpHeaders['Transfer-Encoding'] ) ) == 'chunked' )
|
|
{
|
|
if ( ( $decoded = $this->decodeChunked( $this->content ) ) !== '' )
|
|
{
|
|
$this->content = $decoded;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public function decodeChunked( $body )
|
|
{
|
|
$response = '';
|
|
$initial = $body;
|
|
|
|
for( $response = ''; !empty($body); $body = trim($body) )
|
|
{
|
|
$_pos = \strpos( $body, "\r\n" );
|
|
|
|
/* The last line could be 0000000 which is valid but doesn't have \r\n */
|
|
if( trim( $body, '0' ) === '' )
|
|
{
|
|
return $response;
|
|
}
|
|
|
|
/* Make sure this is valid hex */
|
|
if( $_pos === FALSE OR !preg_match( "/^([a-f0-9]+)$/mi", \substr( $body, 0, $_pos ) ) )
|
|
{
|
|
return $response ?: $initial;
|
|
}
|
|
|
|
$_len = (int) hexdec( \substr( $body, 0, $_pos ) );
|
|
|
|
if( $_len < 1 )
|
|
{
|
|
return $initial;
|
|
}
|
|
|
|
$response .= \substr( $body, $_pos + 2, $_len );
|
|
$body = \substr( $body, $_pos + 2 + $_len );
|
|
$body = trim($body);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Magic Method: String Value
|
|
*
|
|
* @return string
|
|
*/
|
|
public function __toString()
|
|
{
|
|
return $this->content;
|
|
}
|
|
|
|
/**
|
|
* Decode JSON
|
|
*
|
|
* @param bool $asArray Whether to decode as an array or not
|
|
* @return array
|
|
* @throws \RuntimeException
|
|
* @li BAD_JSON
|
|
*/
|
|
public function decodeJson( $asArray=TRUE )
|
|
{
|
|
$json = json_decode( $this->content, $asArray );
|
|
if ( $json === FALSE )
|
|
{
|
|
throw new \RuntimeException('BAD_JSON');
|
|
}
|
|
return $json;
|
|
}
|
|
|
|
/**
|
|
* Decode XML
|
|
*
|
|
* @return \IPS\Xml\SimpleXML
|
|
* @throws \RuntimeException
|
|
* @li BAD_XML
|
|
*/
|
|
public function decodeXml()
|
|
{
|
|
try
|
|
{
|
|
$xml = \IPS\Xml\SimpleXML::loadString( $this->content );
|
|
}
|
|
catch ( \InvalidArgumentException $e )
|
|
{
|
|
throw new \RuntimeException('BAD_XML');
|
|
}
|
|
|
|
return $xml;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @return array
|
|
* @throws \RuntimeException
|
|
* @li EXPECTED_KEY_NOT_PRESENT
|
|
*/
|
|
public function decodeQueryString( $expectedKey=NULL )
|
|
{
|
|
@parse_str( $this->content, $queryString );
|
|
|
|
if ( $expectedKey !== NULL and !array_key_exists( $expectedKey, $queryString ) )
|
|
{
|
|
throw new \RuntimeException( 'EXPECTED_KEY_NOT_PRESENT' );
|
|
}
|
|
|
|
return $queryString;
|
|
}
|
|
} |