HTTP-Promise

 view release on metacpan or  search on metacpan

lib/HTTP/Promise/Entity.pm  view on Meta::CPAN

##----------------------------------------------------------------------------
## Asynchronous HTTP Request and Promise - ~/lib/HTTP/Promise/Entity.pm
## Version v0.2.2
## Copyright(c) 2023 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2022/04/19
## Modified 2025/08/30
## All rights reserved.
## 
## 
## This program is free software; you can redistribute  it  and/or  modify  it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
package HTTP::Promise::Entity;
BEGIN
{
    use strict;
    use warnings;
    warnings::register_categories( 'HTTP::Promise' );
    use parent qw( Module::Generic );
    use vars qw( $VERSION $EXCEPTION_CLASS $BOUNDARY_DELIMITER $BOM2ENC $ENC2BOM $BOM_RE 
                 $BOM_MAX_LENGTH $DEFAULT_MIME_TYPE );
    use Data::UUID;
    use HTTP::Promise::Exception;
    use HTTP::Promise::Headers;
    use HTTP::Promise::Body;
    use Module::Generic::HeaderValue;
    # use Nice::Try;
    use Symbol;
    use URI::Escape::XS ();
    use constant CRLF => "\015\012";
    our $EXCEPTION_CLASS = 'HTTP::Promise::Exception';
    our $BOUNDARY_DELIMITER = "\015\012";
    our $DEFAULT_MIME_TYPE = 'application/octet-stream';
    our $VERSION = 'v0.2.2';
};

use strict;
use warnings;

sub init
{
    my $self = shift( @_ );
    $self->{body}           = undef;
    # Sie minimum from which compression is enabled, if mime type is suitable.
    # Defaults to 200Kb
    $self->{compression_min}= 204800;
    $self->{effective_type} = undef;
    $self->{epilogue}       = undef;
    $self->{ext_vary}       = undef;
    $self->{headers}        = undef;
    $self->{is_encoded}     = 0;
    $self->{output_dir}     = undef;
    $self->{preamble}       = undef;
    $self->{_init_strict_use_sub} = 1;
    $self->{_exception_class} = $EXCEPTION_CLASS;
    $self->SUPER::init( @_ ) || return( $self->pass_error );
    $self->{_parts} = [];
    return( $self );
}

sub add_part
{
    my $self = shift( @_ );
    my( $part, $index ) = @_;
    return( $self->error( "Part provided is not a HTTP::Promise::Entity object." ) ) if( !$self->_is_a( $part => 'HTTP::Promise::Entity' ) );
    my $parts = $self->_parts;
    $index = -1 if( !defined( $index ) );
    $index = $parts->size + 2 + $index if( $index < 0 );
    $parts->splice( $index, 0, $part );
    return( $part );
}

sub as_form_data
{
    my $self = shift( @_ );
    my $type = $self->headers->type;
    return(0) unless( lc( $type ) eq 'multipart/form-data' );
    $self->_load_class( 'HTTP::Promise::Body::Form::Data' ) || return( $self->pass_error );
    my $form = HTTP::Promise::Body::Form::Data->new;
    $form->debug( $self->debug // 0 );
    my $parts = $self->parts;

lib/HTTP/Promise/Entity.pm  view on Meta::CPAN

    $new_body = $body->clone if( defined( $body ) );
    my $parts = $self->parts;
    if( !$parts->is_empty )
    {
        $new_parts = $self->new_array;
        # Each part is an HTTP::Promise::Entity
        for( @$parts )
        {
            my $paddr = $self->_refaddr( $_ );
            # This would be weird, but let's do it anyway
            if( $paddr eq $addr )
            {
                $new_parts->push( $new );
                next;
            }
            my $new_part = $_->clone;
            $new_parts->push( $new_part );
        }
        $new->parts( $new_parts );
    }
    $new->headers( $new_headers ) if( defined( $new_headers ) );
    $new->body( $new_body ) if( defined( $new_body ) );
    $new->name( $self->name ) if( $self->name );
    $new->is_encoded( $self->is_encoded );
    $new->debug( $self->debug );
    $new->preamble( $self->preamble->clone );
    $new->epilogue( $self->epilogue->clone );
    $new->compression_min( $self->compression_min );
    $new->effective_type( $self->effective_type );
    my $msg;
    if( ( $msg = $self->http_message ) && $opts->{clone_message} )
    {
        # To prevent endless recursion
        my $new_msg = $msg->clone( clone_entity => 0 );
        $new_msg->headers( $new_headers );
        $new_msg->entity( $new );
        $new->http_message( $new_msg );
    }
    return( $new );
}

sub compression_min { return( shift->_set_get_number( 'compression_min', @_ ) ); }

# NOTE: an outdated method since nowadays everyone use UTF-8
# This is not intended to be a generic method, but instead to be used specifically for this entity
# content parameter can be provided to avoid reading from the body if we already have data handy.
sub content_charset
{
    my $self = shift( @_ );
    my $opts = $self->_get_args_as_hash( @_ );
    my $headers = $self->headers;
    # If parameter content_type_charset is set to false, this means it was just tried and 
    # we should not try it again.
    if( ( my $charset = $headers->content_type_charset ) && 
        ( !exists( $opts->{content_type_charset} ) || $opts->{content_type_charset} ) )
    {
        return( $charset );
    }

    $self->_load_class( 'Encode' ) || return( $self->pass_error );
    unless( defined( $BOM2ENC ) && scalar( %$BOM2ENC ) )
    {
        # Credits: Matthew Lawrence (File::BOM)
        our $BOM2ENC = +{
            map{ Encode::encode( $_, "\x{feff}" ) => $_ } qw(
                UTF-8
                UTF-16BE
                UTF-16LE
                UTF-32BE
                UTF-32LE
            )
        };

        our $ENC2BOM = +{
            reverse( %$BOM2ENC ),
            map{ $_ => Encode::encode( $_, "\x{feff}" ) } qw(
                UCS-2
                iso-10646-1
                utf8
            )
        };
        my @boms = sort{ length( $b ) <=> length( $a ) } keys( %$BOM2ENC );
        our $BOM_MAX_LENGTH = length( $boms[0] );
        {
            local $" = '|';
            our $BOM_RE = qr/@boms/;
        }
    }
    
    # time to start guessing
    # If called from decoded_content, kind of pointless to call decoded_content again
    my $cref;
    if( exists( $opts->{content} ) && length( $opts->{content} ) )
    {
        return( $self->error( "Unsupported data type (", ref( $opts->{content} ), ")." ) ) if( ref( $opts->{content} ) && !$self->_is_scalar( $opts->{content} ) );
        $cref = $self->_is_scalar( $opts->{content} ) ? $opts->{content} : \$opts->{content};
    }
    else
    {
        my $body = $self->body || return( '' );
        my $io = $body->open( '<', { binmode => 'raw' } ) ||
            return( $self->pass_error( $body->error ) );
        my $buff;
        my $bytes = $io->read( $buff, 4096 );
        return( $self->pass_error( $io->error ) ) if( !defined( $bytes ) );
        return( '' ) if( !$bytes );
        $cref = \$buff;
    }
    
    # Is there a Byte Order Mark?
    if( $$cref =~ /^($BOM_RE)/ )
    {
        my $bom = $1;
        return( $BOM2ENC->{ $bom } );
    }

    # Unicode BOM
    return( 'UTF-8' )    if( $$cref =~ /^\xEF\xBB\xBF/ );
    return( 'UTF-32LE' ) if( $$cref =~ /^\xFF\xFE\x00\x00/ );
    return( 'UTF-32BE' ) if( $$cref =~ /^\x00\x00\xFE\xFF/ );
    return( 'UTF-16LE' ) if( $$cref =~ /^\xFF\xFE/ );
    return( 'UTF-16BE' ) if( $$cref =~ /^\xFE\xFF/ );

    if( $headers->content_is_xml )
    {
        # http://www.w3.org/TR/2006/REC-xml-20060816/#sec-guessing
        # XML entity not accompanied by external encoding information and not
        # in UTF-8 or UTF-16 encoding must begin with an XML encoding declaration,
        # in which the first characters must be '<?xml'
        return( 'UTF-32BE' ) if( $$cref =~ /^\x00\x00\x00</ );
        return( 'UTF-32LE' ) if( $$cref =~ /^<\x00\x00\x00/ );
        return( 'UTF-16BE' ) if( $$cref =~ /^(?:\x00\s)*\x00</ );
        return( 'UTF-16LE' ) if( $$cref =~ /^(?:\s\x00)*<\x00/ );
        if( $$cref =~ /^[[:blank:]\h]*(<\?xml[^\x00]*?\?>)/ )
        {
            if( $1 =~ /[[:blank:]\h\v]encoding[[:blank:]\h\v]*=[[:blank:]\h\v]*(["'])(.*?)\1/ )
            {
                my $enc = $2;
                $enc =~ s/^[[:blank:]\h]+//;
                $enc =~ s/[[:blank:]\h]+\z//;
                return( $enc ) if( $enc );
            }
        }
        return( 'UTF-8' );
    }
    elsif( $headers->content_is_text )
    {
        my $encoding = $self->guess_character_encoding( content => $cref, object => 1 );
        return( ref( $encoding ) ? $encoding->mime_name : $encoding ) if( $encoding );
    }
    elsif( $headers->content_type eq 'application/json' )
    {
        # RFC 4627, ch 3
        return( 'UTF-32BE' ) if( $$cref =~ /^\x00\x00\x00./s );
        return( 'UTF-32LE' ) if( $$cref =~ /^.\x00\x00\x00/s );
        return( 'UTF-16BE' ) if( $$cref =~ /^\x00.\x00./s );
        return( 'UTF-16LE' ) if( $$cref =~ /^.\x00.\x00/s );
        return( 'UTF-8' );
    }
    # if( $headers->content_type =~ /^text\// && $self->_load_class( 'Encode' ) )
    if( $headers->content_type =~ /^text\// )
    {
        if( length( $$cref ) )
        {
            return( 'US-ASCII' ) unless( $$cref =~ /[\x80-\xFF]/ );
            my $encoding;
            # try-catch
            local $@;
            eval
            {
                Encode::decode_utf8( $$cref, ( Encode::FB_CROAK | Encode::LEAVE_SRC ) );
                $encoding = 'UTF-8';
            };
            if( $@ )
            {
                return( $self->error( "Failed to decode utf8 content: $@" ) );
            }



( run in 0.498 second using v1.01-cache-2.11-cpan-e1769b4cff6 )