Seditio Source
Root |
./othercms/ips_4.3.4/system/Http/Ranges.php
<?php
/**
 * @brief        HTTP Ranges Request/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        19 May 2014
 * @note        This class facilitates parsing of HTTP Range requests and sending the appropriate response
 */

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 Ranges Request/Response Class
 */
class _Ranges extends Response
{
   
/**
     * @brief    File object
     */
   
public $file    = NULL;

   
/**
     * @brief    Is this a range request?
     */
   
public $ranged    = FALSE;

   
/**
     * Constructor
     *
     * @param    \IPS\File    $file        File object we are sending
     * @param    int            $throttle    Throttle speed (kb/sec)
     * @return    void
     */
   
public function __construct( $file, $throttle=0 )
    {
       
/* Send some headers */
       
\IPS\Output::i()->sendHeader( 'Accept-Ranges: bytes' );
        \
IPS\Output::i()->sendHeader( "Content-Transfer-Encoding: binary" );

       
/* We will need the file size */
       
$filesize        = $file->filesize();
       
$length            = $filesize;
       
$range            = 0;
       
$start_range    = 0;

        \
IPS\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' );

           
/* Get the range */
           
list( $a, $range )    = explode( "=", $_SERVER['HTTP_RANGE'] );

           
/* Check for multiple ranges */
           
$ranges                = explode( ",", $range );
           
           
/* Did we only find one range? */
           
if( count($ranges) == 1 )
            {
                \
IPS\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 )
                {
                   
$size    = $filesize - 1;
                   
$length = $end_range + 1;
                }
               
/* Or did we want the first x bytes? */
               
else if( \strlen( $end_range ) == 0 )
                {
                   
$size    = $filesize - 1;
                   
$length    = $size - $start_range + 1;
                }
               
/* Or a specific range with start and end specified */
               
else
                {
                   
$size    = $end_range;
                   
$length    = $end_range - $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' );

                    \
IPS\Output::i()->sendStatusCodeHeader( 416 );
                    \
IPS\Output::i()->sendHeader( "Content-Range: */{$filesize}" );
                    return;
                }
               
/* Otherwise send the 206 header */
               
else
                {
                    \
IPS\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}" );
                }
               
               
/* 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 );
                }
            }
           
/* Or did they get froggy and request multiple ranges? */
           
else
            {
               
/* Start parsing the ranges */
               
$the_responses        = array();
               
                foreach(
$ranges as $arange )
                {
                    \
IPS\Log::debug( "Multiple ranges: {$start_range} - {$end_range}", 'ranges' );

                   
/* Get the actual range start and end */
                   
list( $start_range, $end_range )     = explode( "-", $arange );
                   
                   
/* If there is no start, we want the last x bytes */
                   
if( \strlen( $start_range ) == 0 )
                    {
                       
$size    = $filesize - 1;
                       
$length = $end_range + 1;
                    }
                   
/* Or did we want the first x bytes? */
                   
else if( \strlen( $end_range ) == 0 )
                    {
                       
$size    = $filesize - 1;
                       
$length    = $size - $start_range + 1;
                    }
                   
/* Or a specific range with start and end specified */
                   
else
                    {
                       
$size    = $end_range;
                       
$length    = $end_range - $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' );

                        \
IPS\Output::i()->sendStatusCodeHeader( 416 );
                        \
IPS\Output::i()->sendHeader( "Content-Range: */{$filesize}" );
                        return;
                    }
                   
/* Otherwise store this range */
                   
else
                    {
                       
$the_responses[]     = array( $start_range, $size, $length );
                    }
                }

               
/* Did we still only have one range? */
               
if( count($the_responses) == 1 )
                {
                   
$length            = $the_responses[0][2];
                   
$start_range    = $the_responses[0][0];
                   
$size            = $the_responses[0][1];

                   
$the_responses    = array();

                    \
IPS\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}" );

                   
/* 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 );
                    }
                }
               
/* Now we're working with something exciting */
               
else if( count($the_responses) > 1 )
                {
                   
$content_length    = 0;
                   
                    \
IPS\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    += $part[2];
                    }
                   
                   
$content_length    += \strlen( "\r\n--IPDOWNLOADSBOUNDARYMARKER--\r\n" );
                   
                    \
IPS\Output::i()->sendStatusCodeHeader( 206 );
                    \
IPS\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 );
                    }
                }
            }
        }
       
/* No range */
       
else
        {
            \
IPS\Log::debug( "No range requested, sending entire file", 'ranges' );

           
$size = $filesize - 1;

            \
IPS\Output::i()->sendStatusCodeHeader( 200 );

            if( !
ini_get('zlib.output_compression') OR ini_get('zlib.output_compression') == 'off' )
            {
                \
IPS\Output::i()->sendHeader( "Content-Length: " . $filesize );
            }

            \
IPS\Output::i()->sendHeader( "Content-Range: bytes 0-{$size}/{$filesize}" );
        }
       
       
/* Not sending multiple ranges */
       
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 ) );
        }

       
/* Turn off output buffering if it is on */
       
while( ob_get_level() > 0 )
        {
           
ob_end_clean();
        }
       
       
/* Throttling? */
       
$throttle    = ( $throttle > 0 ) ? ( $throttle * 1024 ) : NULL;

       
/* Do we have multiple ranges? */
       
if( isset( $the_responses ) AND count($the_responses) )
        {
           
/* Loop over each range */
           
foreach( $the_responses as $part )
            {
               
$length = $part[2];

                echo
"\r\n--IPDOWNLOADSBOUNDARYMARKER\r\n";
                echo
"Content-Type: " . \IPS\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 );
            }

            echo
"\r\n--IPDOWNLOADSBOUNDARYMARKER--\r\n";
        }
       
/* Just a single range? */
       
else
        {
           
$file->printFile( $start_range, $length, $throttle );
        }
    }

   
/**
     * Determine if this is a range request for the start of the file
     *
     * @return    bool
     */
   
public static function isStartOfFile()
    {
       
/* Was this a ranged request? */
       
if( isset( $_SERVER['HTTP_RANGE'] ) )
        {
           
/* Get the range */
           
list( $a, $range )    = explode( "=", $_SERVER['HTTP_RANGE'] );

           
/* Check for multiple ranges */
           
$ranges                = explode( ",", $range );
           
           
/* Did we only find one range? */
           
if( count($ranges) == 1 )
            {
               
/* Get the actual range start and end */
               
list( $start_range, $end_range )    = explode( "-", $range );
   
                if( \
strlen( $start_range ) == 0 )
                {
                    return
TRUE;
                }
                else if( \
strlen( $end_range ) == 0 )
                {
                    return
FALSE;
                }
                else
                {
                    return (
$start_range == 0 ) ? TRUE : FALSE;
                }
            }
           
/* Or did they get froggy and request multiple ranges? */
           
else
            {
                foreach(
$ranges as $arange )
                {
                   
/* Get the actual range start and end */
                   
list( $start_range, $end_range )     = explode( "-", $arange );
                   
                    if( \
strlen( $start_range ) == 0 )
                    {
                        return
TRUE;
                    }
                    else if( \
strlen( $end_range ) == 0 )
                    {
                        continue;
                    }
                    else
                    {
                        if(
$start_range == 0 )
                        {
                            return
TRUE;
                        }
                    }
                }

                return
FALSE;
            }
        }
       
/* No range */
       
else
        {
            return
TRUE;
        }
    }
}