The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
##############################################################################
package WWW::HostipInfo;

use strict;
use LWP::UserAgent;
use Carp;
use vars qw( $VERSION $DEBUG );

$VERSION = '0.13';

my $GetAPI   = 'http://api.hostip.info/get_html.php?position=true&ip=';
my $RoughAPI = 'http://api.hostip.info/get_html.php?position=true&ip=';


## PUBLIC METHOD

sub new {
    my ( $class, $ip ) = @_;
    my $opts =   ! defined $ip        ? {} 
               : ref( $ip ) eq 'HASH' ? $ip
               : { ip => $ip }
    ;

    my $self  = {
        url => $GetAPI,
        %$opts,
    };

    bless $self, $class;

    $self->_init() unless ( $self->ua );
    $self;
}


sub _init {
	my $self = shift;
	$self->ua( LWP::UserAgent->new(agent => "WWW::HostipInfo/$VERSION") );
}


sub ua {
	$_[0]->{ua} = $_[1] if(@_ > 1);
	$_[0]->{ua}
}


sub ip {
	$_[0]->{ip} = $_[1] if(@_ > 1);
	$_[0]->{ip}
}

sub url {
	$_[0]->{url} = $_[1] if(@_ > 1);
	$_[0]->{url}
}


sub recent_info { $_[0]->{_recent_info}; }


sub get_info {
	my $self = shift;
	my $ip   = shift || $self->ip;
	my $opt  = shift || {};

	my $chk = $self->_check_ipaddr($ip);
	# $chk : 0..invalid ip address, 1..class A - C, 2..class D or E, 3..private
	if(!$chk){
		croak "IP address is required."
	}
	elsif($chk == 2){
		return $self->_get_info_as_null;
	}
	elsif($chk == 3){
		return $self->_get_info_as_private
	}

	$self->ip($ip);

	my $url       = $self->url;
	my $response  = $self->ua->get($url . $ip);

	return unless($response->is_success);

	my $content = $response->content;

	if($DEBUG){ warn $content; }

	return $self->_set_info_to_obj($content);
}


## PRIVATE METHOD

sub _check_ipaddr {
	return 0 if(!defined $_[1] or $_[1] !~ /^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
	return 2 if($1 >= 224); # class D or E
	return   ($1 == 127 or $1 == 10)   ? 3
	       : ($1 == 192 and $2 == 168) ? 3
	       : ($1 == 172 and $2 >= 16 and $2 <= 31) ? 3 : 1;
}


sub _set_info_to_obj {
	my ( $self, $content ) = @_;
	my ( $country_code, $city, $lat, $lon, $region, $name, $code );

    for my $line ( split/\n/, $content ) {
        if ( $line =~ /^Country: (.+?) \(([A-Z][A-Z])\)$/ ) {
            ( $name, $code ) = ( $1, $2 );
        }
        elsif ( $line =~ /^City: ([^,]+)(?:, (\w+))?$/ ) {
            ($city, $region) = ( $1, $2 );
        }
        elsif ( $line =~ /^Latitude: (.+)$/ ) {
            $lat = $1;
        }
        elsif ( $line =~ /^Longitude: (.+)$/ ) {
            $lon = $1;
        }
    }

	my ($unknown_city, $unknown_country) = (0,0);
	if($city =~ /^\([uU]nknown [cC]ity/){
		$city = ''; $region = ''; $unknown_city = 1;
	}
	if($name =~ /^\([uU]nknown [cC]ountry/){
		$name = ''; $unknown_country = 1;
	}

	$self->{_recent_info} = bless {
		_ipaddr      => $self->ip,
		_CountryName => $name,
		_CountryCode => $code,
		_City        => $city,
		_Region      => ($region || ''),
		_Latitude    => $lat,
		_Longitude   => $lon,
		_private     => 0,
		_un_city     => $unknown_city,
		_un_country  => $unknown_country,
	}, 'WWW::HostipInfo::Info';
}


sub _get_info_as_null {
	$_[0]->{_recent_info} = bless {
		_ipaddr      => $_[0]->ip,
		_CountryName => '',
		_CountryCode => 'XX',
		_City        => '',
		_Region      => undef,
		_Latitude    => undef,
		_Longitude   => undef,
		_private     => 0,
		_un_city     => 1,
		_un_country  => 1,
	}, 'WWW::HostipInfo::Info';
}


sub _get_info_as_private {
	$_[0]->{_recent_info} = bless {
		_ipaddr      => $_[0]->ip,
		_CountryName => '',
		_CountryCode => 'XX',
		_City        => '',
		_Region      => undef,
		_Latitude    => undef,
		_Longitude   => undef,
		_private     => 1,
		_un_city     => 1,
		_un_country  => 1,
	}, 'WWW::HostipInfo::Info';
}


##############################################################################
# Information Class
##############################################################################

package WWW::HostipInfo::Info;

sub is_private { $_[0]->{_private}; }

sub is_guessed { $_[0]->{_guessed}; }

sub has_unknown_city { $_[0]->{_un_city}; }

sub has_unknown_country { $_[0]->{_un_country}; }

sub ip { $_[0]->{_ipaddr}; }

sub name { $_[0]->{_CountryName}; }

sub code { $_[0]->{_CountryCode}; }

sub country_name { $_[0]->{_CountryName}; }

sub country_code { $_[0]->{_CountryCode}; }

sub country { $_[0]->{_CountryName}; }

sub city { $_[0]->{_City}; }

sub region { $_[0]->{_Region}; }

sub latitude { $_[0]->{_Latitude}; }

sub longitude { $_[0]->{_Longitude} }

##############################################################################
1;
__END__

=pod

=head1 NAME

WWW::HostipInfo
 - get a country and city information from ip address via www.hostip.info API.


=head1 SYNOPSIS

 use WWW::HostipInfo;

 my $hostip = new WWW::HostipInfo;
 my $ip     = 'xxx.xxx.xxx.xxx';
 my $info   = $hostip->get_info($ip);
 
 if($info->is_private){ warn "This is a private ip address." }
 
 my $country_code = $info->code;
 my $city_name    = $info->city;
 my $region       = $info->region; # if any
 
 $info = $hostip->recent_info->country_name; # fetch most recent data
 
 print WWW::HostipInfo->new($ip)->get_info->city; # shortcut



=head1 DESCRIPTION

This module gets a country and city information from ip address
via L<www.hostip.info> API.


=head1 METHODS

=head2 new

    $hostip = WWW::HostipInfo->new();
    
    $hostip = WWW::HostipInfo->new( $ip );
    
    $hostip = WWW::HostipInfo->new( { ip => $ip, url => $url, ua => $user_agent } );

returns a WWW::HostipInfo object.
This method can take an ip address or a hash reference optionally.

=head2 ip

    $ip = $hostip->ip([$ip]);

setter / getter to the ip address.

=head2 url

    $url = $hostip->url([$url]);

setter / getter to the HostipInfo API.
Default is C<api.hostip.info>.

=head2 ua

    $user_agent = $hostip->ua([$user_agent]);

setter / getter to the user agent.
Default is C<LWP::UserAgent>.

=head2 get_info

    $info = $hostip->get_info();

returns a WWW::HostipInfo::Info object.
If the method can't get a information, will return C<undef>.

=head2 recent_info

    $info = $hostip->get_info();

returns a C<WWW::HostipInfo::Info> object.


=head1 WWW::HostipInfo::Info

With C<get_info()>, WWW::HostipInfo object returns
WWW::HostipInfo::Info object.

=head2 METHODS

getters for informations.

=over 4

=item country_code

return the cuntry code.
If private ip address is used, the code is 'XX'.

=item code

an alias to country_code

=item country_name

return the cuntry name.

=item name

an alias to country_name

=item country

an alias to country_name

=item city

return the city name as long as it is not unknown.

=item region

return state code if the coutnry is US.

=item latitude

=item longitude

=item ip

=item is_private

If private ip address is used, returns true.

=item has_unknown_city

If the object has no data for city, returns true.

=item has_unknown_country

If the object has no data for country, returns true.


=back

=head1 SEE ALSO

L<http://www.hostip.info/>,
L<LWP>

=head1 AUTHOR

Makamaka Hannyaharamitu, E<lt>makamaka[at]cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright 2005, 2009-2012 by Makamaka Hannyaharamitu

This library is licensed under GNU GENERAL PUBLIC LICENSE

=cut