File Coverage

blib/lib/Geo/IP/RU/IpGeoBase.pm
Criterion Covered Total %
statement 9 115 7.8
branch 0 48 0.0
condition 0 14 0.0
subroutine 3 22 13.6
pod 3 19 15.7
total 15 218 6.8


line stmt bran cond sub pod time code
1 1     1   861 use 5.008;
  1         5  
  1         45  
2 1     1   8 use strict;
  1         3  
  1         43  
3 1     1   17 use warnings;
  1         3  
  1         2551  
4              
5             package Geo::IP::RU::IpGeoBase;
6              
7             our $VERSION = '0.04';
8              
9             =head1 NAME
10              
11             Geo::IP::RU::IpGeoBase - look up location by IP address in Russia
12              
13             =head1 DESCRIPTION
14              
15             This module allows you to look up location in DB provided by
16             http://ipgeobase.ru service. Access to the DB is free. Contains
17             information about city, region, federal district and coordinates.
18              
19             DB provided as plain text files and is not very suitable for look
20             ups without loading all data into memory. Instead it's been decided
21             to import data into a database. Use command line utility to create
22             and update back-end DB.
23              
24             At this moment DB can be created in SQLite, mysql and Pg. If
25             you create table manually then probably module will just work.
26             It's very easy to add support for more back-end DBs. Patches are
27             welcome.
28              
29             =head1 METHODS
30              
31             =head2 new
32              
33             Returns a new object. Takes a hash with options, mostly
34             description of the back-end:
35              
36             Geo::IP::RU::IpGeoBase->new( db => {
37             dbh => $dbh, table => 'my_table',
38             } );
39             # or
40             Geo::IP::RU::IpGeoBase->new( db => {
41             dsn => 'dbi:mysql:mydb',
42             user => 'root', pass => 'secret',
43             table => 'my_table',
44             } );
45              
46             =over 4
47              
48             =item * dbh - connected L handle, or you can use dsn.
49              
50             =item * dsn, user, pass - DSN like described in L, for
51             example 'dbi:SQLite:my.db', user name and his password.
52              
53             =item * table - name of the table with data, default
54             is 'ip_geo_base_ru'.
55              
56             =back
57              
58             =cut
59              
60             sub new {
61 0     0 1   my $proto = shift;
62 0   0       my $self = bless { @_ }, ref($proto) || $proto;
63 0           return $self->init;
64             }
65              
66             sub init {
67 0     0 0   my $self = shift;
68              
69 0 0         die "No information about database"
70             unless my $db = $self->{'db'};
71              
72 0 0         unless ( $db->{'dbh'} ) {
73 0 0         die "No dsn and no dbh" unless $db->{'dsn'};
74              
75 0           require DBI;
76 0 0         $db->{'dbh'} = DBI->connect(
77             $db->{'dsn'}, $db->{'user'}, $db->{'pass'},
78             { RaiseError => 0, PrintError => 0 }
79             ) or die "Couldn't connect to the DB: ". DBI->errstr;
80 0           $db->{'dbh'}->do("SET NAMES 'utf8'");
81 0           $db->{'decode'} = 1;
82             } else {
83 0 0         $db->{'decode'} = 1
84             unless exists $db->{'decode'};
85             }
86 0 0         if ( $db->{'decode'} ) {
87 0           require Encode;
88 0           $db->{'decoder'} = Encode::find_encoding('UTF-8');
89             }
90              
91 0 0         $db->{'driver'} = $db->{'dbh'}{'Driver'}{'Name'}
92             or die "Couldn't figure out driver name of the DB";
93              
94 0   0       $db->{'table'} ||= 'ip_geo_base_ru';
95 0           $db->{'quoted_table'} = $db->{'dbh'}->quote_identifier($db->{'table'});
96              
97 0           return $self;
98             }
99              
100             =head2 find_by_ip
101              
102             Takes an IP in 'xxx.xxx.xxx.xxx' format and returns information
103             about blocks that contains this IP. Yep, blocks, not a block.
104             In theory DB may contain intersecting blocks.
105              
106             Each record is a hash reference with the fields matching table
107             columns: istart, iend, start, end, city, region, federal_district,
108             latitude and longitude.
109              
110             =cut
111              
112             sub find_by_ip {
113 0     0 1   my $self = shift;
114 0 0         my $ip = shift or die 'No IP provided';
115 0           my $int = $self->ip2int($ip);
116 0           return $self->intersections( $int, $int, order => 'ASC', @_ );
117             }
118              
119 0     0 0   sub ip2int { return unpack 'N', pack 'C4', split /[.]/, $_[1] }
120 0     0 0   sub int2ip { return join '.', unpack "C4", pack "N", $_[1] }
121              
122             sub intersections {
123 0     0 0   my $self = shift;
124 0           my ($istart, $iend, %rest) = @_;
125 0           my $table = $self->db_info->{'quoted_table'};
126 0           my $dbh = $self->dbh;
127 0           my $query = "SELECT * FROM $table WHERE "
128             . $dbh->quote_identifier('istart') .' <= '. $dbh->quote($iend)
129             .' AND '. $dbh->quote_identifier('iend') .' >= '. $dbh->quote($istart);
130 0 0         $query .= ' ORDER BY iend - istart '. $rest{'order'}
131             if $rest{'order'};
132 0           my $res = $dbh->selectall_arrayref( $query, { Slice => {} } );;
133 0 0 0       die "Couldn't execute '$query': ". $dbh->errstr if !$res && $dbh->errstr;
134 0           return @{ $self->decode( $res ) };
  0            
135             }
136              
137             sub fetch_record {
138 0     0 0   my $self = shift;
139 0           my ($istart, $iend) = @_;
140 0           my $table = $self->db_info->{'quoted_table'};
141 0           my $dbh = $self->dbh;
142 0           my $query = "SELECT * FROM $table WHERE "
143             . $dbh->quote_identifier('istart') .' = '. $dbh->quote($istart)
144             .' AND '. $dbh->quote_identifier('iend') .' = '. $dbh->quote($iend);
145 0           my $res = $self->dbh->selectrow_hashref( $query );
146 0 0 0       die "Couldn't execute '$query': ". $dbh->errstr if !$res && $dbh->errstr;
147 0           return $self->decode( $res );
148             }
149              
150             sub insert_record {
151 0     0 0   my $self = shift;
152 0           my %rec = @_;
153              
154 0           my $table = $self->db_info->{'quoted_table'};
155 0           my @keys = keys %rec;
156 0           my $dbh = $self->dbh;
157 0           my $query =
158             "INSERT INTO $table(". join( ', ', map $dbh->quote_identifier($_), @keys) .")"
159             ." VALUES (". join( ', ', map $dbh->quote( $rec{$_} ), @keys ) .")";
160 0 0         return $dbh->do( $query ) or die "Couldn't execute '$query': ". $dbh->errstr;
161             }
162              
163             sub update_record {
164 0     0 0   my $self = shift;
165 0           my %rec = @_;
166              
167 0           my $table = $self->db_info->{'quoted_table'};
168              
169 0   0       my @keys = grep $_ ne 'istart' && $_ ne 'iend', keys %rec;
170 0           my $dbh = $self->dbh;
171 0           my $query =
172             "UPDATE $table SET "
173             . join(
174             ' AND ',
175             map $dbh->quote_identifier($_) .' = '. $dbh->quote($rec{$_}),
176             @keys
177             )
178             ." WHERE "
179             . join(
180             ' AND ',
181             map $dbh->quote_identifier($_) .' = '. $dbh->quote($rec{$_}),
182             qw(istart iend)
183             );
184 0 0         return $dbh->do( $query ) or die "Couldn't execute '$query': ". $dbh->errstr;
185             }
186              
187             sub delete_record {
188 0     0 0   my $self = shift;
189 0           my ($istart, $iend) = @_;
190 0           my $table = $self->db_info->{'quoted_table'};
191 0           my $dbh = $self->dbh;
192 0           my $query = "DELETE FROM $table WHERE "
193             . $dbh->quote_identifier('istart') .' = '. $dbh->quote($istart)
194             .' AND '. $dbh->quote_identifier('iend') .' = '. $dbh->quote($iend);
195 0 0         return $dbh->do( $query ) or die "Couldn't execute '$query': ". $dbh->errstr;
196             }
197              
198             sub decode {
199 0     0 0   my $self = shift;
200 0           my $value = shift;
201 0 0         return $value unless $self->{'db'}{'decode'};
202 0 0         return $value unless defined $value;
203              
204 0           my $decoder = $self->{'db'}{'decoder'};
205 0 0         foreach my $rec ( ref($value) eq 'ARRAY'? (@$value) : ($value) ) {
206 0           $_ = $decoder->decode($_) foreach grep defined, values %$rec;
207             }
208 0           return $value;
209             }
210              
211             sub process_file {
212 0     0 0   my $self = shift;
213 0 0         my %args = (@_%2? (path => @_) : @_);
214              
215 0           my $file = $args{'path'};
216 0           my @fields = @{ $args{'fields'} };
  0            
217              
218 0 0         open my $fh, '<:encoding(cp1251)', $file
219             or die "Couldn't open $file";
220              
221 0           while ( my $str = <$fh> ) {
222 0           chomp $str;
223              
224 0           my %rec;
225 0           @rec{ @fields } = split /\t/, $str;
226 0           delete $rec{'country'};
227 0 0         @rec{'start', 'end'} = $self->split_block( delete $rec{'block'} )
228             if exists $rec{'block'};
229              
230 0           $args{'callback'}->( \%rec );
231             }
232 0           close $fh;
233             }
234              
235 0     0 0   sub split_block { return split /\s*-\s*/, $_[1], 2; }
236              
237              
238 0     0 0   sub db_info { return $_[0]->{'db'} }
239              
240 0     0 1   sub dbh { return $_[0]->{'db'}{'dbh'} }
241              
242             sub create_table {
243 0     0 0   my $self = shift;
244              
245 0           my $driver = $self->db_info->{'driver'};
246              
247 0           my $call = 'create_'. lc( $driver ) .'_table';
248 0 0         die "Table creation is not supported for $driver"
249             unless $self->can($call);
250              
251 0           return $self->$call();
252             }
253              
254             sub create_sqlite_table {
255 0     0 0   my $self = shift;
256              
257 0           my $table = $self->db_info->{'quoted_table'};
258 0           my $query = <
259             CREATE TABLE $table (
260             istart INTEGER NOT NULL,
261             iend INTEGER NOT NULL,
262             start TEXT NOT NULL,
263             end TEXT NOT NULL,
264             city TEXT,
265             region TEXT,
266             federal_district TEXT,
267             latitude REAL,
268             longitude REAL,
269             in_update INT NOT NULL DEFAULT(0),
270             PRIMARY KEY (istart ASC, iend ASC)
271             )
272             END
273 0 0         return $self->dbh->do( $query )
274             or die "Couldn't execute '$query': ". $self->dbh->errstr;
275             }
276              
277             sub create_mysql_table {
278 0     0 0   my $self = shift;
279 0           my $table = $self->db_info->{'quoted_table'};
280 0           my $query = <
281             CREATE TABLE $table (
282             istart INTEGER UNSIGNED NOT NULL,
283             iend INTEGER UNSIGNED NOT NULL,
284             start VARCHAR(15) NOT NULL,
285             end VARCHAR(15) NOT NULL,
286             city TEXT,
287             region TEXT,
288             federal_district TEXT,
289             latitude FLOAT(8,6),
290             longitude FLOAT(8,6),
291             in_update TINYINT NOT NULL DEFAULT 0,
292             PRIMARY KEY (istart, iend)
293             ) CHARACTER SET 'utf8'
294             END
295 0 0         return $self->dbh->do( $query )
296             or die "Couldn't execute '$query': ". $self->dbh->errstr;
297             }
298              
299             sub create_pg_table {
300 0     0 0   my $self = shift;
301 0           my $table = $self->db_info->{'quoted_table'};
302 0           my $endq = $self->dbh->quote_identifier('end');
303 0           my $query = <
304             CREATE TABLE $table (
305             istart BIGINT NOT NULL,
306             iend BIGINT NOT NULL,
307             start VARCHAR(15) NOT NULL,
308             $endq VARCHAR(15) NOT NULL,
309             city TEXT,
310             region TEXT,
311             federal_district TEXT,
312             latitude NUMERIC(8,6),
313             longitude NUMERIC(8,6),
314             in_update INT2 NOT NULL DEFAULT 0,
315             PRIMARY KEY (istart, iend)
316             )
317             END
318 0 0         return $self->dbh->do( $query )
319             or die "Couldn't execute '$query': ". $self->dbh->errstr;
320             }
321              
322             =head1 AUTHOR
323              
324             Ruslan Zakirov ERuslan.Zakirov@gmail.comE
325              
326             =head1 LICENSE
327              
328             Under the same terms as perl itself.
329              
330             =cut
331              
332             1;